From 2fe3ab0075a9a3013363ef2030d8de015ab06d36 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 30 Oct 2026 22:34:03 +0000 Subject: [PATCH 1/4] feat: optional codebase-context integration for plan and execute MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add off-by-default integrations.codebaseContext config, helper commands that record plan context and edit preflight receipts under .work/, workflow docs, and unit tests with a stubbed CLI runner. Co-authored-by: Patrick Rosselló Colom --- README.md | 4 +- bin/gsdd.mjs | 3 + bin/lib/config.mjs | 30 +- bin/lib/init-runtime.mjs | 10 + bin/lib/integrations-cli.mjs | 86 ++++ bin/lib/integrations/codebase-context.mjs | 386 ++++++++++++++++++ bin/lib/rendering.mjs | 5 + distilled/workflows/execute.md | 4 + distilled/workflows/plan.md | 7 + docs/USER-GUIDE.md | 15 + ...sdd.integrations-codebase-context.test.cjs | 136 ++++++ 11 files changed, 683 insertions(+), 3 deletions(-) create mode 100644 bin/lib/integrations-cli.mjs create mode 100644 bin/lib/integrations/codebase-context.mjs create mode 100644 tests/gsdd.integrations-codebase-context.test.cjs diff --git a/README.md b/README.md index ee5a6f4..5cfa68c 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,9 @@ lifecycle and helper machinery stays in the repository for the agents and for in - [Verification Discipline](docs/VERIFICATION-DISCIPLINE.md) for what the checks do and do not establish. - [Changelog](CHANGELOG.md) for migration and compatibility notes. -Workspine requires Node `>=22`. The compatibility command `workspine init` remains available throughout +README requires Node `>=22`. The compatibility command `workspine init` remains available throughout the `0.35.x` release line. The `gsdd` binary alias is removed in the next minor release. +Optional [codebase-context](https://github.com/PatrickSys/codebase-context) integration (off by default) can enrich `/work-plan` and `/work-execute` when enabled in `.work/config.json`; see [User Guide](docs/USER-GUIDE.md#optional-integrations). + MIT. See [LICENSE](LICENSE). diff --git a/bin/gsdd.mjs b/bin/gsdd.mjs index a0086a6..f3f1e7f 100644 --- a/bin/gsdd.mjs +++ b/bin/gsdd.mjs @@ -16,6 +16,7 @@ import { createCmdNext } from './lib/next.mjs'; import { resolveWorkspaceContext } from './lib/workspace-root.mjs'; import { createCmdGitIdentity } from './lib/git-identity.mjs'; import { maybeShowUpdateNotice } from './lib/update-awareness.mjs'; +import { createCmdIntegrations } from './lib/integrations-cli.mjs'; import { classifyInformationRequest, reportCommandShapeError, validateCommandShape } from './lib/init-runtime.mjs'; import { FRAMEWORK_VERSION } from './lib/workflows.mjs'; const __filename = fileURLToPath(import.meta.url); @@ -32,6 +33,7 @@ const cmdHealth = createCmdHealth(INIT_CONTEXT); const cmdNext = createCmdNext(INIT_CONTEXT); const cmdJourney = createCmdJourney(INIT_CONTEXT); const cmdGitIdentity = createCmdGitIdentity(INIT_CONTEXT); +const cmdIntegrations = createCmdIntegrations(INIT_CONTEXT); const cmdUpdate = (...updateArgs) => { if (updateArgs.includes('--global') || updateArgs.includes('-g')) { @@ -64,6 +66,7 @@ const COMMANDS = { 'phase-status': cmdPhaseStatus, verify: cmdVerify, scaffold: cmdScaffold, + integrations: cmdIntegrations, help: cmdHelp, }; // Setup is the first-run facade; keep it outside the retained compatibility table while help diff --git a/bin/lib/config.mjs b/bin/lib/config.mjs index 2f68f36..c8cd992 100644 --- a/bin/lib/config.mjs +++ b/bin/lib/config.mjs @@ -18,6 +18,13 @@ export const DEFAULT_GIT_PROTOCOL = { pr: 'Follow the existing repo or team review workflow. Do not assume PR creation, timing, or naming unless explicitly requested.', }; +export const DEFAULT_INTEGRATIONS = Object.freeze({ + codebaseContext: Object.freeze({ + enabled: false, + command: 'codebase-context', + }), +}); + // Rigor controls the current workflow's alignment and quality gates. Model profiles // remain a separate cost/quality selection axis. `max` is accepted only for stored // configuration and CLI compatibility; it resolves to the currently implemented @@ -94,7 +101,7 @@ export function normalizeModelProfile(value) { return VALID_MODEL_PROFILES.includes(value) ? value : 'balanced'; } -function buildConfigDefaults({ autoAdvance = false, rigorProfile }) { +function buildConfigDefaults({ autoAdvance = false, rigorProfile, integrations = defaultIntegrations() }) { const rigor = resolveRigor(rigorProfile); const cost = resolveCost('balanced'); const config = { @@ -103,6 +110,7 @@ function buildConfigDefaults({ autoAdvance = false, rigorProfile }) { ...cost, commitDocs: true, gitProtocol: { ...DEFAULT_GIT_PROTOCOL }, + integrations, initVersion: 'v1.1', }; if (autoAdvance) config.autoAdvance = true; @@ -110,7 +118,23 @@ function buildConfigDefaults({ autoAdvance = false, rigorProfile }) { } export function buildDefaultConfig({ autoAdvance = false } = {}) { - return buildConfigDefaults({ autoAdvance, rigorProfile: 'high' }); + return buildConfigDefaults({ autoAdvance, rigorProfile: 'high', integrations: defaultIntegrations() }); +} + +function defaultIntegrations() { + return { + codebaseContext: { ...DEFAULT_INTEGRATIONS.codebaseContext }, + }; +} + +function mergeIntegrations(existing = {}) { + return { + codebaseContext: { + ...DEFAULT_INTEGRATIONS.codebaseContext, + ...(existing.codebaseContext ?? {}), + enabled: existing.codebaseContext?.enabled === true, + }, + }; } function buildExistingConfigDefaults(existing = {}, { autoAdvance = false } = {}) { @@ -139,6 +163,7 @@ export function loadProjectModelConfig(cwd = process.cwd()) { ...defaults, ...existing, workflow: { ...defaults.workflow, ...(existing.workflow ?? {}) }, + integrations: mergeIntegrations(existing.integrations), }; } catch (e) { console.error(`WARNING: ${configPathLabel(cwd)} is malformed (${e.message}). Using defaults.`); @@ -166,6 +191,7 @@ function loadConfigForMutation(cwd = process.cwd()) { ...defaults, ...existing, workflow: { ...defaults.workflow, ...(existing.workflow ?? {}) }, + integrations: mergeIntegrations(existing.integrations), }, }; } catch (e) { diff --git a/bin/lib/init-runtime.mjs b/bin/lib/init-runtime.mjs index a349946..ba3cf78 100644 --- a/bin/lib/init-runtime.mjs +++ b/bin/lib/init-runtime.mjs @@ -291,6 +291,7 @@ export const COMMAND_FLAGS = { next: { '--json': false, '--format': true, '--init': false, '--id': true, '--prompt': true, '--default': true, '--gate': true, '--blocking': true, '--answer': true, '--replace': false, '--title': true, '--body': true, '--supersedes': true, '--privacy': true, '--backlog': true }, models: { '--agent': true, '--profile': true, '--runtime': true, '--model': true }, rigor: {}, + integrations: { '--json': false, '--workspace-root': true, '--files': true, '--plan': true, '--query': true, '--task': true }, }; // Bare words count only in the command position, so a positional argument that happens to read @@ -464,6 +465,7 @@ export function validateCommandShape(command, commandArgs = []) { next: 'Usage: workspine next [graph rebuild|question add|question answer|decision record|dogfood capture] [flags]', models: 'Usage: workspine models [show|profile|agent-profile|clear-agent-profile|set|clear] [flags]', rigor: 'Usage: workspine rigor [show|low|medium|high|max| ]', + integrations: 'Usage: workspine integrations [show] | codebase-context [flags]', }[command]; if (!usage) return null; @@ -522,6 +524,14 @@ export function validateCommandShape(command, commandArgs = []) { || RIGOR_STEP_NAMES.has(first) && RIGOR_LEVEL_NAMES.has(second) && rest.length === 0; if (!valid) return shapeError(command, usage); } + if (command === 'integrations') { + const valid = positionals.length === 0 + || positionals[0] === 'show' + || (positionals[0] === 'codebase-context' + && positionals.length >= 2 + && ['plan-context', 'edit-preflight', 'enable', 'disable', 'show'].includes(positionals[1])); + if (!valid) return shapeError(command, usage); + } return commandSpecificFlagError(command, positionals, commandArgs); } diff --git a/bin/lib/integrations-cli.mjs b/bin/lib/integrations-cli.mjs new file mode 100644 index 0000000..e20a0c9 --- /dev/null +++ b/bin/lib/integrations-cli.mjs @@ -0,0 +1,86 @@ +import { + cmdCodebaseContextSubcommand, + createCodebaseContextRunner, + showIntegrationStatus, +} from './integrations/codebase-context.mjs'; +import { resolveWorkspaceContext } from './workspace-root.mjs'; +import { validateCommandShape } from './init-runtime.mjs'; +import { execFileSync } from 'child_process'; +function resolveIntegrationsRoot(flagArgs, cwd) { + const workspace = resolveWorkspaceContext(flagArgs, { cwd }); + if (workspace.invalid) { + console.error(`ERROR: ${workspace.error}`); + process.exitCode = 1; + return null; + } + try { + assertStateAuthority(workspace.state); + } catch (error) { + console.error(`ERROR: ${error.message}`); + process.exitCode = 1; + return null; + } + return { workspaceRoot: workspace.workspaceRoot, flagArgs: workspace.args ?? flagArgs }; +} + +function positionalArgs(flagArgs) { + const skip = new Set(['--json', '--workspace-root']); + const out = []; + for (let i = 0; i < flagArgs.length; i += 1) { + const token = flagArgs[i]; + if (skip.has(token)) { + if (token === '--workspace-root') i += 1; + continue; + } + if (token.startsWith('--')) { + if (['--files', '--plan', '--query', '--task'].includes(token)) i += 1; + continue; + } + out.push(token); + } + return out; +} + +export function createCmdIntegrations({ cwd = process.cwd(), runner = null } = {}) { + const defaultRunner = runner ?? createCodebaseContextRunner({ execFile: execFileSync }); + + return function cmdIntegrations(...integrationArgs) { + const grammarError = validateCommandShape('integrations', integrationArgs); + if (grammarError) { + console.error(grammarError); + process.exitCode = 1; + return; + } + + const json = integrationArgs.includes('--json'); + const workspaceRoot = resolveIntegrationsRoot(integrationArgs, cwd); + if (!workspaceRoot) return; + + const positionals = positionalArgs(workspaceRoot.flagArgs); + const [first, second, ...rest] = positionals; + + if (!first || first === 'show') { + const config = loadProjectModelConfig(workspaceRoot.workspaceRoot); + const status = showIntegrationStatus({ config, workspaceRoot: workspaceRoot.workspaceRoot, runner: defaultRunner }); + if (json) output(status); + else { + const cc = status.integrations.codebaseContext; + console.log(`codebase-context: ${cc.enabled ? 'enabled' : 'disabled'} (cli: ${cc.cli_available ? 'available' : 'missing'})`); + console.log(` command: ${cc.command}`); + console.log(` artifacts: ${cc.artifact_dir}`); + } + return; + } + + if (first === 'codebase-context') { + return cmdCodebaseContextSubcommand(second, [...rest, ...(json ? ['--json'] : [])], { + cwd: workspaceRoot.workspaceRoot, + json, + runner: defaultRunner, + }); + } + + console.error('Usage: workspine integrations [show] | codebase-context [flags]'); + process.exitCode = 1; + }; +} diff --git a/bin/lib/integrations/codebase-context.mjs b/bin/lib/integrations/codebase-context.mjs new file mode 100644 index 0000000..c2475c5 --- /dev/null +++ b/bin/lib/integrations/codebase-context.mjs @@ -0,0 +1,386 @@ +import { execFileSync } from 'child_process'; +import { existsSync, mkdirSync, writeFileSync } from 'fs'; +import { dirname, join, relative, resolve } from 'path'; +import { createHash } from 'crypto'; +import { DEFAULT_INTEGRATIONS, loadProjectModelConfig, writeProjectConfig } from '../config.mjs'; +import { output, parseFlagValue } from '../cli-utils.mjs'; +import { resolveStateDir } from '../state-dir.mjs'; +import { writeFileAtomic } from '../atomic-write.mjs'; + +export const INTEGRATION_DIR = 'integrations/codebase-context'; + +function normalizeSlashes(value) { + return String(value || '').replace(/\\/g, '/'); +} + +export function mergeCodebaseContextIntegration(config = {}) { + const existing = config?.integrations?.codebaseContext ?? {}; + return { + ...DEFAULT_INTEGRATIONS.codebaseContext, + ...existing, + enabled: existing.enabled === true, + }; +} + +export function integrationArtifactDir(workDir) { + return join(workDir, INTEGRATION_DIR); +} + +function slugify(value) { + return String(value || 'task') + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, '') + .slice(0, 48) || 'task'; +} + +function parseFilesFlag(flagArgs) { + const raw = parseFlagValue(flagArgs, '--files').value; + if (!raw) return []; + return raw.split(',').map((part) => normalizeSlashes(part).trim()).filter(Boolean); +} + +function directoryScopes(files, workspaceRoot) { + const scopes = new Set(); + for (const file of files) { + const absolute = resolve(workspaceRoot, file); + const rel = normalizeSlashes(relative(workspaceRoot, absolute)); + if (!rel || rel.startsWith('..')) continue; + const dir = rel.includes('/') ? rel.slice(0, rel.lastIndexOf('/')) : '.'; + scopes.add(dir === '' ? '.' : dir); + } + return [...scopes].slice(0, 12); +} + +function parseCliJson(stdout) { + const trimmed = String(stdout || '').trim(); + if (!trimmed) return null; + try { + return JSON.parse(trimmed); + } catch { + return { raw: trimmed }; + } +} + +export function createCodebaseContextRunner({ execFile, exists = existsSync } = {}) { + if (typeof execFile !== 'function') { + throw new Error('createCodebaseContextRunner requires execFile'); + } + + function probeCli(command, cwd) { + try { + execFile(command, ['--version'], { + cwd, + encoding: 'utf-8', + stdio: ['ignore', 'pipe', 'pipe'], + windowsHide: true, + }); + return true; + } catch { + return false; + } + } + + function runCli(command, args, cwd) { + return execFile(command, args, { + cwd, + encoding: 'utf-8', + stdio: ['ignore', 'pipe', 'pipe'], + windowsHide: true, + env: { ...process.env, CODEBASE_ROOT: cwd }, + }); + } + + return { probeCli, runCli, exists }; +} + +export function buildSkippedReceipt(reason, extra = {}) { + return { + schema_version: 1, + integration: 'codebase-context', + status: reason === 'disabled' ? 'disabled' : 'unavailable', + reason, + ...extra, + }; +} + +export function runPlanContext({ + workspaceRoot, + workDir, + config, + files = [], + planPath = null, + json = false, + now = new Date(), + runner, +} = {}) { + const integration = mergeCodebaseContextIntegration(config); + const artifactPath = join(integrationArtifactDir(workDir), 'plan-context.json'); + const relativeArtifact = normalizeSlashes(relative(workspaceRoot, artifactPath)); + + if (!integration.enabled) { + const body = buildSkippedReceipt('disabled', { artifact: relativeArtifact, files, plan_path: planPath }); + if (json) output(body); + else console.log('codebase-context integration is disabled in .work/config.json'); + return body; + } + + const command = integration.command || DEFAULT_INTEGRATIONS.codebaseContext.command; + if (!runner.probeCli(command, workspaceRoot)) { + const body = buildSkippedReceipt('cli_not_found', { + command, + artifact: relativeArtifact, + files, + plan_path: planPath, + }); + if (json) output(body); + else console.log(`codebase-context CLI not found (${command}); skipping plan context`); + return body; + } + + if (files.length === 0) { + const body = { + schema_version: 1, + integration: 'codebase-context', + status: 'error', + error: 'missing_files', + message: 'plan-context requires --files with at least one repo-relative path', + artifact: relativeArtifact, + }; + if (json) output(body); + else console.error(body.message); + process.exitCode = 1; + return body; + } + + const patternsStdout = runner.runCli(command, ['patterns', '--json'], workspaceRoot); + const patterns = parseCliJson(patternsStdout); + + const scopes = directoryScopes(files, workspaceRoot); + const scopedSearches = []; + for (const scope of scopes) { + const query = scope === '.' + ? 'team patterns and conventions for repository root files' + : `team patterns and conventions for files under ${scope}`; + const stdout = runner.runCli(command, [ + 'search', + '--query', + query, + '--intent', + 'explore', + '--json', + '--limit', + '5', + ], workspaceRoot); + scopedSearches.push({ + scope, + query, + response: parseCliJson(stdout), + }); + } + + const body = { + schema_version: 1, + integration: 'codebase-context', + status: 'ok', + recorded_at: now.toISOString(), + command, + plan_path: planPath, + files, + directory_scopes: scopes, + patterns, + scoped_searches: scopedSearches, + artifact: relativeArtifact, + }; + + mkdirSync(dirname(artifactPath), { recursive: true }); + writeFileAtomic(artifactPath, `${JSON.stringify(body, null, 2)}\n`); + if (json) output(body); + else console.log(`Recorded plan context at ${relativeArtifact}`); + return body; +} + +export function runEditPreflight({ + workspaceRoot, + workDir, + config, + files = [], + query, + taskId = null, + json = false, + now = new Date(), + runner, +} = {}) { + const integration = mergeCodebaseContextIntegration(config); + const safeTask = slugify(taskId || now.toISOString()); + const artifactPath = join(integrationArtifactDir(workDir), 'preflight', `${safeTask}.json`); + const relativeArtifact = normalizeSlashes(relative(workspaceRoot, artifactPath)); + const logPath = join(integrationArtifactDir(workDir), 'preflight-log.jsonl'); + + if (!integration.enabled) { + const body = buildSkippedReceipt('disabled', { + artifact: relativeArtifact, + files, + query: query || null, + task_id: taskId, + }); + if (json) output(body); + else console.log('codebase-context integration is disabled in .work/config.json'); + return body; + } + + const command = integration.command || DEFAULT_INTEGRATIONS.codebaseContext.command; + if (!runner.probeCli(command, workspaceRoot)) { + const body = buildSkippedReceipt('cli_not_found', { + command, + artifact: relativeArtifact, + files, + query: query || null, + task_id: taskId, + }); + if (json) output(body); + else console.log(`codebase-context CLI not found (${command}); skipping edit preflight`); + return body; + } + + if (!String(query || '').trim()) { + const body = { + schema_version: 1, + integration: 'codebase-context', + status: 'error', + error: 'missing_query', + message: 'edit-preflight requires --query describing the intended edit', + artifact: relativeArtifact, + }; + if (json) output(body); + else console.error(body.message); + process.exitCode = 1; + return body; + } + + const fileHint = files.length > 0 ? ` Files: ${files.join(', ')}.` : ''; + const searchQuery = `${String(query).trim()}.${fileHint}`; + const stdout = runner.runCli(command, [ + 'search', + '--query', + searchQuery, + '--intent', + 'edit', + '--json', + '--limit', + '10', + ], workspaceRoot); + const response = parseCliJson(stdout); + + const body = { + schema_version: 1, + integration: 'codebase-context', + status: 'ok', + recorded_at: now.toISOString(), + command, + task_id: taskId, + files, + query: String(query).trim(), + search_query: searchQuery, + response, + preflight: response?.preflight ?? null, + artifact: relativeArtifact, + digest: createHash('sha256').update(JSON.stringify(response ?? {})).digest('hex'), + }; + + mkdirSync(dirname(artifactPath), { recursive: true }); + writeFileAtomic(artifactPath, `${JSON.stringify(body, null, 2)}\n`); + writeFileSync(logPath, `${JSON.stringify({ + recorded_at: body.recorded_at, + task_id: taskId, + files, + query: body.query, + ready: body.preflight?.ready ?? null, + artifact: relativeArtifact, + })}\n`, { flag: 'a' }); + writeFileAtomic(join(integrationArtifactDir(workDir), 'last-preflight.json'), `${JSON.stringify(body, null, 2)}\n`); + + if (json) output(body); + else console.log(`Recorded edit preflight at ${relativeArtifact}`); + return body; +} + +export function showIntegrationStatus({ config, workspaceRoot, runner } = {}) { + const integration = mergeCodebaseContextIntegration(config); + const command = integration.command || DEFAULT_INTEGRATIONS.codebaseContext.command; + const cliAvailable = integration.enabled ? runner.probeCli(command, workspaceRoot) : false; + return { + schema_version: 1, + integrations: { + codebaseContext: { + enabled: integration.enabled, + command, + cli_available: cliAvailable, + artifact_dir: `.work/${INTEGRATION_DIR}`, + }, + }, + }; +} + +export function setIntegrationEnabled(cwd, enabled, { writeConfig = writeProjectConfig, loadConfig = loadProjectModelConfig } = {}) { + const config = loadConfig(cwd); + config.integrations = config.integrations || {}; + config.integrations.codebaseContext = { + ...mergeCodebaseContextIntegration(config), + enabled: Boolean(enabled), + }; + writeConfig(config, cwd); + return config.integrations.codebaseContext; +} + +export function cmdCodebaseContextSubcommand(subcommand, flagArgs, { + cwd = process.cwd(), + json = flagArgs.includes('--json'), + runner = null, + loadConfig = loadProjectModelConfig, +} = {}) { + const workspace = resolveStateDir(cwd); + const workspaceRoot = resolve(workspace.dir, '..'); + const workDir = workspace.dir; + const config = loadConfig(workspaceRoot); + const execRunner = runner ?? createCodebaseContextRunner({ execFile: execFileSync }); + + switch (subcommand) { + case 'plan-context': + return runPlanContext({ + workspaceRoot, + workDir, + config, + files: parseFilesFlag(flagArgs), + planPath: parseFlagValue(flagArgs, '--plan').value, + json, + runner: execRunner, + }); + case 'edit-preflight': + return runEditPreflight({ + workspaceRoot, + workDir, + config, + files: parseFilesFlag(flagArgs), + query: parseFlagValue(flagArgs, '--query').value, + taskId: parseFlagValue(flagArgs, '--task').value, + json, + runner: execRunner, + }); + case 'enable': + setIntegrationEnabled(workspaceRoot, true, { loadConfig, writeConfig: writeProjectConfig }); + console.log(' - enabled integrations.codebaseContext'); + return null; + case 'disable': + setIntegrationEnabled(workspaceRoot, false, { loadConfig, writeConfig: writeProjectConfig }); + console.log(' - disabled integrations.codebaseContext'); + return null; + case 'show': + output(showIntegrationStatus({ config, workspaceRoot, runner: execRunner })); + return null; + default: + console.error('Usage: integrations codebase-context [flags]'); + process.exitCode = 1; + return null; + } +} diff --git a/bin/lib/rendering.mjs b/bin/lib/rendering.mjs index 2e85795..e83a84a 100644 --- a/bin/lib/rendering.mjs +++ b/bin/lib/rendering.mjs @@ -74,6 +74,7 @@ import { cmdPhaseStatus, cmdVerify } from './lib/phase.mjs'; import { buildControlMap } from './lib/control-map.mjs'; import { cmdDecisionsQuery, cmdRememberCandidate } from './lib/decision-cli.mjs'; import { createCmdNext } from './lib/next.mjs'; +import { createCmdIntegrations } from './lib/integrations-cli.mjs'; import { maybeShowUpdateNotice } from './lib/update-awareness.mjs'; import { bootstrapHelperWorkspace, consumeWorkspaceRootArg, resolveWorkspaceContext } from './lib/workspace-root.mjs'; import { assertStateAuthority } from './lib/state-dir.mjs'; @@ -87,6 +88,7 @@ const PACKAGE_NAME = ${embeddedPackageName}; const PACKAGE_VERSION = ${embeddedPackageVersion}; const cmdNext = createCmdNext(HELPER_CONTEXT); const cmdGitIdentity = createCmdGitIdentity(HELPER_CONTEXT); +const cmdIntegrations = createCmdIntegrations(HELPER_CONTEXT); function cmdControlMap(...controlArgs) { const context = resolveWorkspaceContext([], { cwd: HELPER_CONTEXT.cwd }); @@ -121,6 +123,7 @@ const COMMANDS = { remember: cmdRememberCandidate, verify: cmdVerify, next: cmdNext, + integrations: cmdIntegrations, }; const RESOLVER_BASED_COMMANDS = new Set([ @@ -166,6 +169,8 @@ function printHelp() { ' Example: node ${helperPath} lifecycle-transition verify --plan .work/phases/{phase_dir}/{plan_id}-PLAN.md --artifact .work/phases/{phase_dir}/{plan_id}-SUMMARY.md --authority workflow --json', ' next [--json] [--format auto|json|human]', ' Route to the next safe Workspine action from ${normalizeStateDirName(stateDirName)}, brownfield, planning, and repo truth', + ' integrations [show] | codebase-context ', + ' Optional codebase-context bridge (off unless enabled in ${normalizeStateDirName(stateDirName)}/config.json)', ' supported package runtime floor: Node >=22', ' next/verify update notice: sequential/best-effort network-bounded checks; no lock or cross-process concurrency guarantee; use --no-update-notice, WORKSPINE_UPDATE_AWARENESS=0, or the legacy GSDD_UPDATE_AWARENESS=0 to opt out', ' health and update remain network-free; run \`npx -y workspine update\` for explicit repair', diff --git a/distilled/workflows/execute.md b/distilled/workflows/execute.md index 66437af..921865d 100644 --- a/distilled/workflows/execute.md +++ b/distilled/workflows/execute.md @@ -94,12 +94,16 @@ For each task in the plan, follow this loop: ```text 1. Read the plan frontmatter and current task. 2. Read the task_scoped files and focused references needed for that task. +2b. Optional codebase-context edit preflight (off by default): when `integrations.codebaseContext.enabled` is true in `.work/config.json` and the CLI is installed, before mutating files for this task run `node .work/bin/gsdd.mjs integrations codebase-context edit-preflight --files --query "" --task --json`. Read `.work/integrations/codebase-context/last-preflight.json` when `status` is `ok`; honor `preflight` warnings but do not treat them as a hard gate unless the plan says so. When `status` is `disabled` or `unavailable`, skip with no behavior change. 3. For roadmap phases, before implementation, run `node .work/bin/gsdd.mjs lifecycle-transition execute --plan .work/phases/{phase_dir}/{plan_id}-PLAN.md --authority workflow --json`; stop if it refuses, then implement the task action. Do not pass `--approved` or `--approval-ref` here: explicit approval is an owner-only transition, and workflow execution consumes the plan's already-durable approval. 4. Run the task's verify steps. 5. Handle any git actions using repo or user conventions. 6. Re-read the plan's Objective and `non_goals`. Confirm the work just completed serves the objective and crossed no boundary; if not, record a deviation before continuing. 7. Record task completion in your working notes and final SUMMARY.md. ``` + +Same optional bridge as `/work-plan`: enable with `integrations.codebaseContext.enabled: true` after installing [codebase-context](https://github.com/PatrickSys/codebase-context). Artifacts live under `.work/integrations/codebase-context/`. + ### Frontmatter And Task Semantics The executor consumes the plan schema defined by `/work-plan`: - frontmatter keys: `phase`, `plan`, `type`, `wave`, `depends_on`, `files-modified`, `autonomous`, `requirements`, `browser_proof_required`, `browser_proof_rationale`, `must_haves`, `tdd` diff --git a/distilled/workflows/plan.md b/distilled/workflows/plan.md index 5815e17..81dbf8e 100644 --- a/distilled/workflows/plan.md +++ b/distilled/workflows/plan.md @@ -96,6 +96,13 @@ Use these sections: - The phase uses only technologies already established in previous phases Quality gate: do not proceed to goal-backward planning if you have unresolved uncertainties about the implementation approach. If research was skipped (skip conditions above apply), document the skip reason in the plan Notes section so the plan checker can verify the skip was justified. + +Optional bridge to the [codebase-context](https://github.com/PatrickSys/codebase-context) CLI when it is installed and enabled in \`.work/config.json\` (\`integrations.codebaseContext.enabled: true\`). Default is off; when disabled or the CLI is missing, skip this block with no behavior change. +After the planned write set is known (repo-relative paths the tasks will touch), run: +\`node .work/bin/gsdd.mjs integrations codebase-context plan-context --files --plan --json\` +Read \`.work/integrations/codebase-context/plan-context.json\` when \`status\` is \`ok\` and fold relevant team patterns into planning context. When \`status\` is \`disabled\` or \`unavailable\`, continue without codebase-context. +Enable once per repo with \`node .work/bin/gsdd.mjs integrations codebase-context enable\` after installing the CLI (\`npx -y codebase-context\` or a local checkout). + ### When This Runs After research_check, before goal_backward_planning. diff --git a/docs/USER-GUIDE.md b/docs/USER-GUIDE.md index 57bf355..4b46f22 100644 --- a/docs/USER-GUIDE.md +++ b/docs/USER-GUIDE.md @@ -452,6 +452,12 @@ only brief-driven `SPEC.md`/`ROADMAP.md` bootstrap; it does not authorize or cha "branch": "Follow existing repo conventions", "commit": "Logical grouping, no phase/task IDs", "pr": "Follow existing review workflow" + }, + "integrations": { + "codebaseContext": { + "enabled": false, + "command": "codebase-context" + } } } ``` @@ -477,6 +483,15 @@ Each adds quality but costs tokens and time: | `workflow.planCheck` | `true` | Fresh-context adversarial plan checking (max-3 cycle loop) | | `workflow.verifier` | `true` | 3-level verification gate after execution | +### Optional integrations + +| Setting | Default | What it Controls | +|---------|---------|------------------| +| `integrations.codebaseContext.enabled` | `false` | When `true` and the CLI is on `PATH`, `/work-plan` and `/work-execute` may call [codebase-context](https://github.com/PatrickSys/codebase-context) helpers and record receipts under `.work/integrations/codebase-context/` | +| `integrations.codebaseContext.command` | `codebase-context` | Executable used for `patterns` / `search --intent edit` calls | + +Inspect status with `node .work/bin/gsdd.mjs integrations show --json`. Enable with `node .work/bin/gsdd.mjs integrations codebase-context enable` after installing the CLI. When disabled or unavailable, workflows skip the bridge with no behavior change. + Disable these to speed up phases in familiar domains or when conserving tokens. Disabling `planCheck` engages reduced-assurance mode: the planner self-checks without the independent reviewer. ### Model Control diff --git a/tests/gsdd.integrations-codebase-context.test.cjs b/tests/gsdd.integrations-codebase-context.test.cjs new file mode 100644 index 0000000..c71183c --- /dev/null +++ b/tests/gsdd.integrations-codebase-context.test.cjs @@ -0,0 +1,136 @@ +const { test } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('fs'); +const path = require('path'); +const { createTempProject, cleanup, readJson, runCliAsMain } = require('./gsdd.helpers.cjs'); +const { pathToFileURL } = require('url'); + +async function importIntegrationModule() { + return import(`${pathToFileURL(path.join(__dirname, '..', 'bin', 'lib', 'integrations', 'codebase-context.mjs')).href}?t=${Date.now()}`); +} + +async function importConfigModule() { + return import(`${pathToFileURL(path.join(__dirname, '..', 'bin', 'lib', 'config.mjs')).href}?t=${Date.now()}`); +} + +function initWorkDir(tmp, { enabled = false } = {}) { + const workDir = path.join(tmp, '.work'); + fs.mkdirSync(workDir, { recursive: true }); + fs.writeFileSync(path.join(workDir, 'config.json'), `${JSON.stringify({ + rigorProfile: 'medium', + integrations: { codebaseContext: { enabled, command: 'stub-codebase-context' } }, + }, null, 2)}\n`); + return workDir; +} + +function stubRunner() { + const calls = []; + return { + calls, + probeCli: () => true, + runCli: (_command, args) => { + calls.push(args); + if (args[0] === 'patterns') return JSON.stringify({ teamPatterns: [{ name: 'Vitest', adoption: 0.9 }] }); + if (args[0] === 'search') { + const intent = args[args.indexOf('--intent') + 1]; + if (intent === 'edit') { + return JSON.stringify({ preflight: { ready: true, patterns: { do: ['match repo style'] } }, results: [] }); + } + return JSON.stringify({ results: [{ filePath: 'src/example.ts' }] }); + } + return '{}'; + }, + exists: () => true, + }; +} + +test('default config keeps codebase-context integration off', async () => { + const { buildDefaultConfig } = await importConfigModule(); + const config = buildDefaultConfig(); + assert.equal(config.integrations.codebaseContext.enabled, false); +}); + +test('plan-context skips when disabled', async () => { + const tmp = createTempProject(); + try { + const workDir = initWorkDir(tmp, { enabled: false }); + const { runPlanContext } = await importIntegrationModule(); + const body = runPlanContext({ + workspaceRoot: tmp, + workDir, + config: readJson(path.join(workDir, 'config.json')), + files: ['src/a.ts'], + json: true, + runner: stubRunner(), + }); + assert.equal(body.status, 'disabled'); + assert.equal(fs.existsSync(path.join(workDir, 'integrations', 'codebase-context', 'plan-context.json')), false); + } finally { + cleanup(tmp); + } +}); + +test('plan-context records patterns and scoped search when enabled', async () => { + const tmp = createTempProject(); + try { + const workDir = initWorkDir(tmp, { enabled: true }); + const runner = stubRunner(); + const { runPlanContext } = await importIntegrationModule(); + const body = runPlanContext({ + workspaceRoot: tmp, + workDir, + config: readJson(path.join(workDir, 'config.json')), + files: ['src/features/a.ts', 'src/features/b.ts'], + planPath: '.work/phases/01-PLAN.md', + json: true, + runner, + }); + assert.equal(body.status, 'ok'); + assert.ok(runner.calls.some((args) => args[0] === 'patterns')); + assert.ok(runner.calls.some((args) => args[0] === 'search' && args.includes('explore'))); + const artifact = readJson(path.join(workDir, 'integrations', 'codebase-context', 'plan-context.json')); + assert.equal(artifact.status, 'ok'); + assert.deepEqual(artifact.files, ['src/features/a.ts', 'src/features/b.ts']); + } finally { + cleanup(tmp); + } +}); + +test('edit-preflight records preflight receipt when enabled', async () => { + const tmp = createTempProject(); + try { + const workDir = initWorkDir(tmp, { enabled: true }); + const runner = stubRunner(); + const { runEditPreflight } = await importIntegrationModule(); + const body = runEditPreflight({ + workspaceRoot: tmp, + workDir, + config: readJson(path.join(workDir, 'config.json')), + files: ['src/a.ts'], + query: 'add validation helper', + taskId: 'task-1', + json: true, + runner, + }); + assert.equal(body.status, 'ok'); + assert.equal(body.preflight.ready, true); + assert.ok(fs.existsSync(path.join(workDir, 'integrations', 'codebase-context', 'preflight', 'task-1.json'))); + assert.ok(fs.existsSync(path.join(workDir, 'integrations', 'codebase-context', 'last-preflight.json'))); + assert.ok(fs.existsSync(path.join(workDir, 'integrations', 'codebase-context', 'preflight-log.jsonl'))); + } finally { + cleanup(tmp); + } +}); + +test('integrations show via CLI on fresh project reports disabled', async () => { + const tmp = createTempProject(); + try { + initWorkDir(tmp); + const result = await runCliAsMain(tmp, ['integrations', 'show', '--json']); + assert.equal(result.exitCode, 0); + const payload = JSON.parse(result.stdout.trim()); + assert.equal(payload.integrations.codebaseContext.enabled, false); + } finally { + cleanup(tmp); + } +}); From be61a66d1fa1a9e83a3210c123cf10976a1cca00 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 30 Oct 2026 22:39:03 +0000 Subject: [PATCH 2/4] fix: map integration to real codebase-context CLI commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Document Workspine-only plan-context/edit-preflight helpers, invoke patterns and search --intent explore|edit (MCP get_team_patterns / search_codebase), bundle integration modules in .work/bin, forward CLI flags correctly, and run node for .js command paths. Tests mirror real JSON shapes; optional CC_E2E_BIN e2e passes against main build. Co-authored-by: Patrick Rosselló Colom --- bin/gsdd.mjs | 2 +- bin/lib/config.mjs | 24 +-- bin/lib/integrations-cli.mjs | 44 +++++- bin/lib/integrations-config.mjs | 64 ++++++++ bin/lib/integrations/codebase-context.mjs | 147 +++++++++--------- bin/lib/rendering.mjs | 5 +- distilled/workflows/execute.md | 2 +- distilled/workflows/plan.md | 2 +- docs/USER-GUIDE.md | 2 + ...sdd.integrations-codebase-context.test.cjs | 109 +++++++++++-- 10 files changed, 286 insertions(+), 115 deletions(-) create mode 100644 bin/lib/integrations-config.mjs diff --git a/bin/gsdd.mjs b/bin/gsdd.mjs index f3f1e7f..df4e3a8 100644 --- a/bin/gsdd.mjs +++ b/bin/gsdd.mjs @@ -33,7 +33,7 @@ const cmdHealth = createCmdHealth(INIT_CONTEXT); const cmdNext = createCmdNext(INIT_CONTEXT); const cmdJourney = createCmdJourney(INIT_CONTEXT); const cmdGitIdentity = createCmdGitIdentity(INIT_CONTEXT); -const cmdIntegrations = createCmdIntegrations(INIT_CONTEXT); +const cmdIntegrations = createCmdIntegrations({ getCwd: () => INIT_CONTEXT.cwd }); const cmdUpdate = (...updateArgs) => { if (updateArgs.includes('--global') || updateArgs.includes('-g')) { diff --git a/bin/lib/config.mjs b/bin/lib/config.mjs index c8cd992..953858d 100644 --- a/bin/lib/config.mjs +++ b/bin/lib/config.mjs @@ -11,6 +11,9 @@ import { parseFlagValue, output } from './cli-utils.mjs'; import { validateCommandShape } from './init-runtime.mjs'; import { assertStateAuthority, resolveStateDir } from './state-dir.mjs'; import { resolveWorkspaceContext } from './workspace-root.mjs'; +import { DEFAULT_INTEGRATIONS, mergeIntegrations } from './integrations-config.mjs'; + +export { DEFAULT_INTEGRATIONS }; export const DEFAULT_GIT_PROTOCOL = { branch: 'Follow the existing repo or team branching convention. Use a feature branch for significant changes when no convention exists.', @@ -18,13 +21,6 @@ export const DEFAULT_GIT_PROTOCOL = { pr: 'Follow the existing repo or team review workflow. Do not assume PR creation, timing, or naming unless explicitly requested.', }; -export const DEFAULT_INTEGRATIONS = Object.freeze({ - codebaseContext: Object.freeze({ - enabled: false, - command: 'codebase-context', - }), -}); - // Rigor controls the current workflow's alignment and quality gates. Model profiles // remain a separate cost/quality selection axis. `max` is accepted only for stored // configuration and CLI compatibility; it resolves to the currently implemented @@ -122,19 +118,7 @@ export function buildDefaultConfig({ autoAdvance = false } = {}) { } function defaultIntegrations() { - return { - codebaseContext: { ...DEFAULT_INTEGRATIONS.codebaseContext }, - }; -} - -function mergeIntegrations(existing = {}) { - return { - codebaseContext: { - ...DEFAULT_INTEGRATIONS.codebaseContext, - ...(existing.codebaseContext ?? {}), - enabled: existing.codebaseContext?.enabled === true, - }, - }; + return mergeIntegrations({}); } function buildExistingConfigDefaults(existing = {}, { autoAdvance = false } = {}) { diff --git a/bin/lib/integrations-cli.mjs b/bin/lib/integrations-cli.mjs index e20a0c9..6d44163 100644 --- a/bin/lib/integrations-cli.mjs +++ b/bin/lib/integrations-cli.mjs @@ -3,9 +3,14 @@ import { createCodebaseContextRunner, showIntegrationStatus, } from './integrations/codebase-context.mjs'; +import { loadProjectModelConfig } from './integrations-config.mjs'; +import { output } from './cli-utils.mjs'; +import { assertStateAuthority } from './state-dir.mjs'; import { resolveWorkspaceContext } from './workspace-root.mjs'; -import { validateCommandShape } from './init-runtime.mjs'; import { execFileSync } from 'child_process'; + +const CC_SUBCOMMANDS = new Set(['plan-context', 'edit-preflight', 'enable', 'disable', 'show']); + function resolveIntegrationsRoot(flagArgs, cwd) { const workspace = resolveWorkspaceContext(flagArgs, { cwd }); if (workspace.invalid) { @@ -41,11 +46,37 @@ function positionalArgs(flagArgs) { return out; } -export function createCmdIntegrations({ cwd = process.cwd(), runner = null } = {}) { +function subcommandFlagArgs(integrationArgs) { + const positionals = positionalArgs(integrationArgs); + const [first, second] = positionals; + const drop = new Set(['integrations', first, second].filter(Boolean)); + const out = []; + for (let i = 0; i < integrationArgs.length; i += 1) { + const token = integrationArgs[i]; + if (drop.has(token)) continue; + if (token === '--workspace-root') { + i += 1; + continue; + } + out.push(token); + } + return out; +} + +function validateIntegrationsShape(integrationArgs) { + const positionals = positionalArgs(integrationArgs); + const [first, second] = positionals; + if (positionals.length === 0 || first === 'show') return null; + if (first === 'codebase-context' && CC_SUBCOMMANDS.has(second)) return null; + return 'Usage: workspine integrations [show] | codebase-context [flags]'; +} + +export function createCmdIntegrations({ cwd = process.cwd(), getCwd = null, runner = null } = {}) { const defaultRunner = runner ?? createCodebaseContextRunner({ execFile: execFileSync }); + const resolveCwd = () => (typeof getCwd === 'function' ? getCwd() : cwd); return function cmdIntegrations(...integrationArgs) { - const grammarError = validateCommandShape('integrations', integrationArgs); + const grammarError = validateIntegrationsShape(integrationArgs); if (grammarError) { console.error(grammarError); process.exitCode = 1; @@ -53,7 +84,7 @@ export function createCmdIntegrations({ cwd = process.cwd(), runner = null } = { } const json = integrationArgs.includes('--json'); - const workspaceRoot = resolveIntegrationsRoot(integrationArgs, cwd); + const workspaceRoot = resolveIntegrationsRoot(integrationArgs, resolveCwd()); if (!workspaceRoot) return; const positionals = positionalArgs(workspaceRoot.flagArgs); @@ -68,12 +99,15 @@ export function createCmdIntegrations({ cwd = process.cwd(), runner = null } = { console.log(`codebase-context: ${cc.enabled ? 'enabled' : 'disabled'} (cli: ${cc.cli_available ? 'available' : 'missing'})`); console.log(` command: ${cc.command}`); console.log(` artifacts: ${cc.artifact_dir}`); + console.log(' bridge: Workspine plan-context → CC `patterns` + `search --intent explore`; edit-preflight → `search --intent edit`'); } return; } if (first === 'codebase-context') { - return cmdCodebaseContextSubcommand(second, [...rest, ...(json ? ['--json'] : [])], { + const flagArgs = subcommandFlagArgs(workspaceRoot.flagArgs); + if (json && !flagArgs.includes('--json')) flagArgs.push('--json'); + return cmdCodebaseContextSubcommand(second, flagArgs, { cwd: workspaceRoot.workspaceRoot, json, runner: defaultRunner, diff --git a/bin/lib/integrations-config.mjs b/bin/lib/integrations-config.mjs new file mode 100644 index 0000000..d17e957 --- /dev/null +++ b/bin/lib/integrations-config.mjs @@ -0,0 +1,64 @@ +import { existsSync, readFileSync, writeFileSync } from 'fs'; +import { join } from 'path'; +import { resolveStateDir } from './state-dir.mjs'; + +export const DEFAULT_INTEGRATIONS = Object.freeze({ + codebaseContext: Object.freeze({ + enabled: false, + command: 'codebase-context', + }), +}); + +export function mergeIntegrations(existing = {}) { + const cc = existing.codebaseContext ?? {}; + return { + codebaseContext: { + ...DEFAULT_INTEGRATIONS.codebaseContext, + ...cc, + enabled: cc.enabled === true, + }, + }; +} + +export function mergeCodebaseContextIntegration(config = {}) { + return mergeIntegrations(config.integrations).codebaseContext; +} + +function readConfigObject(cwd) { + const configPath = join(resolveStateDir(cwd).dir, 'config.json'); + if (!existsSync(configPath)) return null; + try { + return JSON.parse(readFileSync(configPath, 'utf-8')); + } catch { + return null; + } +} + +/** Minimal config surface for the generated `.work/bin` helper (no full models/rigor stack). */ +export function loadProjectModelConfig(cwd = process.cwd()) { + const existing = readConfigObject(cwd); + return { + integrations: mergeIntegrations(existing?.integrations), + }; +} + +export function writeProjectConfig(config, cwd = process.cwd()) { + const configPath = join(resolveStateDir(cwd).dir, 'config.json'); + const existing = readConfigObject(cwd) ?? {}; + writeFileSync(configPath, JSON.stringify({ ...existing, ...config, integrations: config.integrations ?? mergeIntegrations(existing.integrations) }, null, 2)); +} + +export function patchIntegrations(cwd, patch) { + const configPath = join(resolveStateDir(cwd).dir, 'config.json'); + const existing = readConfigObject(cwd) ?? {}; + const nextIntegrations = mergeIntegrations(existing.integrations); + if (patch.codebaseContext) { + nextIntegrations.codebaseContext = { + ...nextIntegrations.codebaseContext, + ...patch.codebaseContext, + enabled: patch.codebaseContext.enabled === true, + }; + } + writeFileSync(configPath, `${JSON.stringify({ ...existing, integrations: nextIntegrations }, null, 2)}\n`); + return nextIntegrations.codebaseContext; +} diff --git a/bin/lib/integrations/codebase-context.mjs b/bin/lib/integrations/codebase-context.mjs index c2475c5..b9d4d85 100644 --- a/bin/lib/integrations/codebase-context.mjs +++ b/bin/lib/integrations/codebase-context.mjs @@ -2,25 +2,39 @@ import { execFileSync } from 'child_process'; import { existsSync, mkdirSync, writeFileSync } from 'fs'; import { dirname, join, relative, resolve } from 'path'; import { createHash } from 'crypto'; -import { DEFAULT_INTEGRATIONS, loadProjectModelConfig, writeProjectConfig } from '../config.mjs'; +import { + DEFAULT_INTEGRATIONS, + loadProjectModelConfig, + mergeCodebaseContextIntegration, + patchIntegrations, +} from '../integrations-config.mjs'; import { output, parseFlagValue } from '../cli-utils.mjs'; import { resolveStateDir } from '../state-dir.mjs'; import { writeFileAtomic } from '../atomic-write.mjs'; export const INTEGRATION_DIR = 'integrations/codebase-context'; +/** + * Workspine helper subcommands (plan-context, edit-preflight) are not codebase-context CLI + * commands. They invoke the published CLI, which wraps MCP tools: + * - plan-context → `patterns` (get_team_patterns) + per-file `search --intent explore` (search_codebase) + * - edit-preflight → `search --intent edit` (search_codebase preflight card) + */ +export const CODEBASE_CONTEXT_BRIDGE = Object.freeze({ + planContext: Object.freeze([ + { argv: ['patterns', '--json'], mcpTool: 'get_team_patterns' }, + { argv: ['search', '--query', '', '--intent', 'explore', '--json', '--limit', '3'], mcpTool: 'search_codebase' }, + ]), + editPreflight: Object.freeze([ + { argv: ['search', '--query', '', '--intent', 'edit', '--json', '--limit', '10'], mcpTool: 'search_codebase' }, + ]), +}); + function normalizeSlashes(value) { return String(value || '').replace(/\\/g, '/'); } -export function mergeCodebaseContextIntegration(config = {}) { - const existing = config?.integrations?.codebaseContext ?? {}; - return { - ...DEFAULT_INTEGRATIONS.codebaseContext, - ...existing, - enabled: existing.enabled === true, - }; -} +export { mergeCodebaseContextIntegration }; export function integrationArtifactDir(workDir) { return join(workDir, INTEGRATION_DIR); @@ -40,18 +54,6 @@ function parseFilesFlag(flagArgs) { return raw.split(',').map((part) => normalizeSlashes(part).trim()).filter(Boolean); } -function directoryScopes(files, workspaceRoot) { - const scopes = new Set(); - for (const file of files) { - const absolute = resolve(workspaceRoot, file); - const rel = normalizeSlashes(relative(workspaceRoot, absolute)); - if (!rel || rel.startsWith('..')) continue; - const dir = rel.includes('/') ? rel.slice(0, rel.lastIndexOf('/')) : '.'; - scopes.add(dir === '' ? '.' : dir); - } - return [...scopes].slice(0, 12); -} - function parseCliJson(stdout) { const trimmed = String(stdout || '').trim(); if (!trimmed) return null; @@ -62,6 +64,22 @@ function parseCliJson(stdout) { } } +function resolveCliInvocation(command, args) { + if (command.endsWith('.js')) { + return { executable: process.execPath, argv: [command, ...args], displayCommand: command }; + } + return { executable: command, argv: args, displayCommand: command }; +} + +function recordInvocation(invocations, command, argv, mcpTool) { + const { executable, argv: execArgv, displayCommand } = resolveCliInvocation(command, argv); + invocations.push({ + command: displayCommand, + argv: executable === process.execPath ? [process.execPath, ...execArgv] : [displayCommand, ...argv], + mcpTool, + }); +} + export function createCodebaseContextRunner({ execFile, exists = existsSync } = {}) { if (typeof execFile !== 'function') { throw new Error('createCodebaseContextRunner requires execFile'); @@ -69,7 +87,8 @@ export function createCodebaseContextRunner({ execFile, exists = existsSync } = function probeCli(command, cwd) { try { - execFile(command, ['--version'], { + const { executable, argv } = resolveCliInvocation(command, ['--version']); + execFile(executable, argv, { cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], @@ -82,7 +101,8 @@ export function createCodebaseContextRunner({ execFile, exists = existsSync } = } function runCli(command, args, cwd) { - return execFile(command, args, { + const { executable, argv } = resolveCliInvocation(command, args); + return execFile(executable, argv, { cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], @@ -91,7 +111,7 @@ export function createCodebaseContextRunner({ execFile, exists = existsSync } = }); } - return { probeCli, runCli, exists }; + return { probeCli, runCli, exists, resolveCliInvocation }; } export function buildSkippedReceipt(reason, extra = {}) { @@ -100,6 +120,7 @@ export function buildSkippedReceipt(reason, extra = {}) { integration: 'codebase-context', status: reason === 'disabled' ? 'disabled' : 'unavailable', reason, + bridge: CODEBASE_CONTEXT_BRIDGE, ...extra, }; } @@ -117,6 +138,7 @@ export function runPlanContext({ const integration = mergeCodebaseContextIntegration(config); const artifactPath = join(integrationArtifactDir(workDir), 'plan-context.json'); const relativeArtifact = normalizeSlashes(relative(workspaceRoot, artifactPath)); + const ccInvocations = []; if (!integration.enabled) { const body = buildSkippedReceipt('disabled', { artifact: relativeArtifact, files, plan_path: planPath }); @@ -146,6 +168,7 @@ export function runPlanContext({ error: 'missing_files', message: 'plan-context requires --files with at least one repo-relative path', artifact: relativeArtifact, + bridge: CODEBASE_CONTEXT_BRIDGE, }; if (json) output(body); else console.error(body.message); @@ -153,29 +176,19 @@ export function runPlanContext({ return body; } - const patternsStdout = runner.runCli(command, ['patterns', '--json'], workspaceRoot); - const patterns = parseCliJson(patternsStdout); - - const scopes = directoryScopes(files, workspaceRoot); - const scopedSearches = []; - for (const scope of scopes) { - const query = scope === '.' - ? 'team patterns and conventions for repository root files' - : `team patterns and conventions for files under ${scope}`; - const stdout = runner.runCli(command, [ - 'search', - '--query', - query, - '--intent', - 'explore', - '--json', - '--limit', - '5', - ], workspaceRoot); - scopedSearches.push({ - scope, + const patternsArgv = ['patterns', '--json']; + recordInvocation(ccInvocations, command, patternsArgv, 'get_team_patterns'); + const patterns = parseCliJson(runner.runCli(command, patternsArgv, workspaceRoot)); + + const fileSearches = []; + for (const file of files.slice(0, 8)) { + const query = `implementation and conventions for ${file}`; + const searchArgv = ['search', '--query', query, '--intent', 'explore', '--json', '--limit', '3']; + recordInvocation(ccInvocations, command, searchArgv, 'search_codebase'); + fileSearches.push({ + file, query, - response: parseCliJson(stdout), + response: parseCliJson(runner.runCli(command, searchArgv, workspaceRoot)), }); } @@ -185,11 +198,12 @@ export function runPlanContext({ status: 'ok', recorded_at: now.toISOString(), command, + bridge: CODEBASE_CONTEXT_BRIDGE, + codebase_context_invocations: ccInvocations, plan_path: planPath, files, - directory_scopes: scopes, patterns, - scoped_searches: scopedSearches, + file_searches: fileSearches, artifact: relativeArtifact, }; @@ -216,6 +230,7 @@ export function runEditPreflight({ const artifactPath = join(integrationArtifactDir(workDir), 'preflight', `${safeTask}.json`); const relativeArtifact = normalizeSlashes(relative(workspaceRoot, artifactPath)); const logPath = join(integrationArtifactDir(workDir), 'preflight-log.jsonl'); + const ccInvocations = []; if (!integration.enabled) { const body = buildSkippedReceipt('disabled', { @@ -251,6 +266,7 @@ export function runEditPreflight({ error: 'missing_query', message: 'edit-preflight requires --query describing the intended edit', artifact: relativeArtifact, + bridge: CODEBASE_CONTEXT_BRIDGE, }; if (json) output(body); else console.error(body.message); @@ -260,17 +276,9 @@ export function runEditPreflight({ const fileHint = files.length > 0 ? ` Files: ${files.join(', ')}.` : ''; const searchQuery = `${String(query).trim()}.${fileHint}`; - const stdout = runner.runCli(command, [ - 'search', - '--query', - searchQuery, - '--intent', - 'edit', - '--json', - '--limit', - '10', - ], workspaceRoot); - const response = parseCliJson(stdout); + const searchArgv = ['search', '--query', searchQuery, '--intent', 'edit', '--json', '--limit', '10']; + recordInvocation(ccInvocations, command, searchArgv, 'search_codebase'); + const response = parseCliJson(runner.runCli(command, searchArgv, workspaceRoot)); const body = { schema_version: 1, @@ -278,6 +286,8 @@ export function runEditPreflight({ status: 'ok', recorded_at: now.toISOString(), command, + bridge: CODEBASE_CONTEXT_BRIDGE, + codebase_context_invocations: ccInvocations, task_id: taskId, files, query: String(query).trim(), @@ -308,7 +318,7 @@ export function runEditPreflight({ export function showIntegrationStatus({ config, workspaceRoot, runner } = {}) { const integration = mergeCodebaseContextIntegration(config); const command = integration.command || DEFAULT_INTEGRATIONS.codebaseContext.command; - const cliAvailable = integration.enabled ? runner.probeCli(command, workspaceRoot) : false; + const cliAvailable = runner.probeCli(command, workspaceRoot); return { schema_version: 1, integrations: { @@ -317,20 +327,14 @@ export function showIntegrationStatus({ config, workspaceRoot, runner } = {}) { command, cli_available: cliAvailable, artifact_dir: `.work/${INTEGRATION_DIR}`, + bridge: CODEBASE_CONTEXT_BRIDGE, }, }, }; } -export function setIntegrationEnabled(cwd, enabled, { writeConfig = writeProjectConfig, loadConfig = loadProjectModelConfig } = {}) { - const config = loadConfig(cwd); - config.integrations = config.integrations || {}; - config.integrations.codebaseContext = { - ...mergeCodebaseContextIntegration(config), - enabled: Boolean(enabled), - }; - writeConfig(config, cwd); - return config.integrations.codebaseContext; +export function setIntegrationEnabled(cwd, enabled) { + return patchIntegrations(cwd, { codebaseContext: { enabled: Boolean(enabled) } }); } export function cmdCodebaseContextSubcommand(subcommand, flagArgs, { @@ -368,11 +372,11 @@ export function cmdCodebaseContextSubcommand(subcommand, flagArgs, { runner: execRunner, }); case 'enable': - setIntegrationEnabled(workspaceRoot, true, { loadConfig, writeConfig: writeProjectConfig }); + setIntegrationEnabled(workspaceRoot, true); console.log(' - enabled integrations.codebaseContext'); return null; case 'disable': - setIntegrationEnabled(workspaceRoot, false, { loadConfig, writeConfig: writeProjectConfig }); + setIntegrationEnabled(workspaceRoot, false); console.log(' - disabled integrations.codebaseContext'); return null; case 'show': @@ -380,6 +384,7 @@ export function cmdCodebaseContextSubcommand(subcommand, flagArgs, { return null; default: console.error('Usage: integrations codebase-context [flags]'); + console.error(' (Workspine helpers; codebase-context CLI: patterns, search --intent explore|edit, map, …)'); process.exitCode = 1; return null; } diff --git a/bin/lib/rendering.mjs b/bin/lib/rendering.mjs index e83a84a..1d75c35 100644 --- a/bin/lib/rendering.mjs +++ b/bin/lib/rendering.mjs @@ -23,6 +23,9 @@ const HELPER_LIB_FILES = Object.freeze([ 'work-context.mjs', 'workflows.mjs', 'workspace-root.mjs', + 'integrations-config.mjs', + 'integrations-cli.mjs', + 'integrations/codebase-context.mjs', ]); const DEFAULT_STATE_DIR_NAME = '.work'; @@ -88,7 +91,7 @@ const PACKAGE_NAME = ${embeddedPackageName}; const PACKAGE_VERSION = ${embeddedPackageVersion}; const cmdNext = createCmdNext(HELPER_CONTEXT); const cmdGitIdentity = createCmdGitIdentity(HELPER_CONTEXT); -const cmdIntegrations = createCmdIntegrations(HELPER_CONTEXT); +const cmdIntegrations = createCmdIntegrations({ getCwd: () => HELPER_CONTEXT.cwd }); function cmdControlMap(...controlArgs) { const context = resolveWorkspaceContext([], { cwd: HELPER_CONTEXT.cwd }); diff --git a/distilled/workflows/execute.md b/distilled/workflows/execute.md index 921865d..d49d54e 100644 --- a/distilled/workflows/execute.md +++ b/distilled/workflows/execute.md @@ -94,7 +94,7 @@ For each task in the plan, follow this loop: ```text 1. Read the plan frontmatter and current task. 2. Read the task_scoped files and focused references needed for that task. -2b. Optional codebase-context edit preflight (off by default): when `integrations.codebaseContext.enabled` is true in `.work/config.json` and the CLI is installed, before mutating files for this task run `node .work/bin/gsdd.mjs integrations codebase-context edit-preflight --files --query "" --task --json`. Read `.work/integrations/codebase-context/last-preflight.json` when `status` is `ok`; honor `preflight` warnings but do not treat them as a hard gate unless the plan says so. When `status` is `disabled` or `unavailable`, skip with no behavior change. +2b. Optional codebase-context edit preflight (off by default): when `integrations.codebaseContext.enabled` is true in `.work/config.json` and the CLI is installed, before mutating files for this task run `node .work/bin/gsdd.mjs integrations codebase-context edit-preflight --files --query "" --task --json`. `edit-preflight` is a Workspine helper that calls `codebase-context search --intent edit` (MCP `search_codebase` preflight card). Read `.work/integrations/codebase-context/last-preflight.json` when `status` is `ok`; honor `preflight` warnings but do not treat them as a hard gate unless the plan says so. When `status` is `disabled` or `unavailable`, skip with no behavior change. 3. For roadmap phases, before implementation, run `node .work/bin/gsdd.mjs lifecycle-transition execute --plan .work/phases/{phase_dir}/{plan_id}-PLAN.md --authority workflow --json`; stop if it refuses, then implement the task action. Do not pass `--approved` or `--approval-ref` here: explicit approval is an owner-only transition, and workflow execution consumes the plan's already-durable approval. 4. Run the task's verify steps. 5. Handle any git actions using repo or user conventions. diff --git a/distilled/workflows/plan.md b/distilled/workflows/plan.md index 81dbf8e..b1e83fe 100644 --- a/distilled/workflows/plan.md +++ b/distilled/workflows/plan.md @@ -97,7 +97,7 @@ Use these sections: Quality gate: do not proceed to goal-backward planning if you have unresolved uncertainties about the implementation approach. If research was skipped (skip conditions above apply), document the skip reason in the plan Notes section so the plan checker can verify the skip was justified. -Optional bridge to the [codebase-context](https://github.com/PatrickSys/codebase-context) CLI when it is installed and enabled in \`.work/config.json\` (\`integrations.codebaseContext.enabled: true\`). Default is off; when disabled or the CLI is missing, skip this block with no behavior change. +Optional bridge to the [codebase-context](https://github.com/PatrickSys/codebase-context) CLI when it is installed and enabled in \`.work/config.json\` (\`integrations.codebaseContext.enabled: true\`). Default is off; when disabled or the CLI is missing, skip this block with no behavior change. \`plan-context\` is a Workspine helper name (not a codebase-context subcommand); it runs \`codebase-context patterns\` (MCP \`get_team_patterns\`) and per-file \`codebase-context search --intent explore\` (MCP \`search_codebase\`). After the planned write set is known (repo-relative paths the tasks will touch), run: \`node .work/bin/gsdd.mjs integrations codebase-context plan-context --files --plan --json\` Read \`.work/integrations/codebase-context/plan-context.json\` when \`status\` is \`ok\` and fold relevant team patterns into planning context. When \`status\` is \`disabled\` or \`unavailable\`, continue without codebase-context. diff --git a/docs/USER-GUIDE.md b/docs/USER-GUIDE.md index 4b46f22..989dcd8 100644 --- a/docs/USER-GUIDE.md +++ b/docs/USER-GUIDE.md @@ -492,6 +492,8 @@ Each adds quality but costs tokens and time: Inspect status with `node .work/bin/gsdd.mjs integrations show --json`. Enable with `node .work/bin/gsdd.mjs integrations codebase-context enable` after installing the CLI. When disabled or unavailable, workflows skip the bridge with no behavior change. +`plan-context` and `edit-preflight` are Workspine helper subcommands only. They call the real codebase-context CLI: `patterns` and `search --intent explore` for planning (MCP `get_team_patterns` / `search_codebase`), and `search --intent edit` before edits (MCP `search_codebase` preflight). + Disable these to speed up phases in familiar domains or when conserving tokens. Disabling `planCheck` engages reduced-assurance mode: the planner self-checks without the independent reviewer. ### Model Control diff --git a/tests/gsdd.integrations-codebase-context.test.cjs b/tests/gsdd.integrations-codebase-context.test.cjs index c71183c..adea98e 100644 --- a/tests/gsdd.integrations-codebase-context.test.cjs +++ b/tests/gsdd.integrations-codebase-context.test.cjs @@ -13,30 +13,55 @@ async function importConfigModule() { return import(`${pathToFileURL(path.join(__dirname, '..', 'bin', 'lib', 'config.mjs')).href}?t=${Date.now()}`); } -function initWorkDir(tmp, { enabled = false } = {}) { +function initWorkDir(tmp, { enabled = false, command = 'stub-codebase-context' } = {}) { const workDir = path.join(tmp, '.work'); fs.mkdirSync(workDir, { recursive: true }); fs.writeFileSync(path.join(workDir, 'config.json'), `${JSON.stringify({ rigorProfile: 'medium', - integrations: { codebaseContext: { enabled, command: 'stub-codebase-context' } }, + integrations: { codebaseContext: { enabled, command } }, }, null, 2)}\n`); return workDir; } +/** Shapes aligned with codebase-context main CLI JSON (SearchResponse / PatternResponse). */ function stubRunner() { const calls = []; + const patternsPayload = { + status: 'success', + patterns: {}, + goldenFiles: [], + }; + const explorePayload = { + status: 'success', + searchQuality: { status: 'ok', confidence: 0.42 }, + budget: { mode: 'compact', resultCount: 1 }, + results: [{ + file: 'src/example.ts:1-10', + summary: "typescript function 'example' in example.ts.", + score: 0.5, + }], + }; + const editPayload = { + status: 'success', + searchQuality: { status: 'low_confidence', confidence: 0.17 }, + budget: { mode: 'compact', resultCount: 2 }, + preflight: { + ready: false, + abstain: true, + nextAction: 'Search quality is low. Refine query or add concrete symbols before editing.', + impact: { coverage: '1/1 callers in results', files: ['src/validate.ts'] }, + }, + results: [{ file: 'src/math.ts:1-3', summary: "typescript function 'add' in math.ts.", score: 0.25 }], + }; return { calls, probeCli: () => true, runCli: (_command, args) => { calls.push(args); - if (args[0] === 'patterns') return JSON.stringify({ teamPatterns: [{ name: 'Vitest', adoption: 0.9 }] }); + if (args[0] === 'patterns') return JSON.stringify(patternsPayload); if (args[0] === 'search') { const intent = args[args.indexOf('--intent') + 1]; - if (intent === 'edit') { - return JSON.stringify({ preflight: { ready: true, patterns: { do: ['match repo style'] } }, results: [] }); - } - return JSON.stringify({ results: [{ filePath: 'src/example.ts' }] }); + return JSON.stringify(intent === 'edit' ? editPayload : explorePayload); } return '{}'; }, @@ -64,13 +89,14 @@ test('plan-context skips when disabled', async () => { runner: stubRunner(), }); assert.equal(body.status, 'disabled'); + assert.ok(body.bridge); assert.equal(fs.existsSync(path.join(workDir, 'integrations', 'codebase-context', 'plan-context.json')), false); } finally { cleanup(tmp); } }); -test('plan-context records patterns and scoped search when enabled', async () => { +test('plan-context records patterns and per-file explore search when enabled', async () => { const tmp = createTempProject(); try { const workDir = initWorkDir(tmp, { enabled: true }); @@ -86,17 +112,20 @@ test('plan-context records patterns and scoped search when enabled', async () => runner, }); assert.equal(body.status, 'ok'); - assert.ok(runner.calls.some((args) => args[0] === 'patterns')); - assert.ok(runner.calls.some((args) => args[0] === 'search' && args.includes('explore'))); + assert.ok(body.codebase_context_invocations.some((row) => row.mcpTool === 'get_team_patterns')); + assert.ok(body.codebase_context_invocations.some((row) => row.mcpTool === 'search_codebase')); + assert.equal(body.patterns.status, 'success'); + assert.equal(body.file_searches.length, 2); + assert.equal(body.file_searches[0].response.status, 'success'); + assert.ok(Array.isArray(body.file_searches[0].response.results)); const artifact = readJson(path.join(workDir, 'integrations', 'codebase-context', 'plan-context.json')); assert.equal(artifact.status, 'ok'); - assert.deepEqual(artifact.files, ['src/features/a.ts', 'src/features/b.ts']); } finally { cleanup(tmp); } }); -test('edit-preflight records preflight receipt when enabled', async () => { +test('edit-preflight records search_codebase preflight when enabled', async () => { const tmp = createTempProject(); try { const workDir = initWorkDir(tmp, { enabled: true }); @@ -113,10 +142,11 @@ test('edit-preflight records preflight receipt when enabled', async () => { runner, }); assert.equal(body.status, 'ok'); - assert.equal(body.preflight.ready, true); + assert.equal(body.response.status, 'success'); + assert.equal(typeof body.preflight.ready, 'boolean'); + assert.equal(body.preflight.abstain, true); + assert.ok(runner.calls.some((args) => args.includes('edit'))); assert.ok(fs.existsSync(path.join(workDir, 'integrations', 'codebase-context', 'preflight', 'task-1.json'))); - assert.ok(fs.existsSync(path.join(workDir, 'integrations', 'codebase-context', 'last-preflight.json'))); - assert.ok(fs.existsSync(path.join(workDir, 'integrations', 'codebase-context', 'preflight-log.jsonl'))); } finally { cleanup(tmp); } @@ -130,6 +160,55 @@ test('integrations show via CLI on fresh project reports disabled', async () => assert.equal(result.exitCode, 0); const payload = JSON.parse(result.stdout.trim()); assert.equal(payload.integrations.codebaseContext.enabled, false); + assert.ok(payload.integrations.codebaseContext.bridge); + } finally { + cleanup(tmp); + } +}); + +test('e2e against built codebase-context when CC_E2E_BIN is set', { skip: !process.env.CC_E2E_BIN }, async () => { + const ccBin = process.env.CC_E2E_BIN; + const tmp = createTempProject(); + try { + fs.mkdirSync(path.join(tmp, 'src'), { recursive: true }); + fs.writeFileSync(path.join(tmp, 'src', 'math.ts'), 'export function add(a: number, b: number) { return a + b; }\n'); + fs.writeFileSync(path.join(tmp, 'package.json'), '{"name":"e2e","type":"module"}\n'); + const { execFileSync } = require('child_process'); + const reindex = ccBin.endsWith('.js') + ? { file: process.execPath, args: [ccBin, 'reindex', '--no-embeddings'] } + : { file: ccBin, args: ['reindex', '--no-embeddings'] }; + execFileSync(reindex.file, reindex.args, { cwd: tmp, encoding: 'utf-8', env: { ...process.env, CODEBASE_ROOT: tmp } }); + + const workDir = initWorkDir(tmp, { enabled: true, command: ccBin }); + const { runPlanContext, runEditPreflight } = await importIntegrationModule(); + const { createCodebaseContextRunner } = await importIntegrationModule(); + const runner = createCodebaseContextRunner({ execFile: execFileSync }); + const config = readJson(path.join(workDir, 'config.json')); + + const plan = runPlanContext({ + workspaceRoot: tmp, + workDir, + config, + files: ['src/math.ts'], + json: true, + runner, + }); + assert.equal(plan.status, 'ok'); + assert.equal(plan.patterns.status, 'success'); + + const preflight = runEditPreflight({ + workspaceRoot: tmp, + workDir, + config, + files: ['src/math.ts'], + query: 'change add function', + taskId: 'e2e-task', + json: true, + runner, + }); + assert.equal(preflight.status, 'ok'); + assert.equal(preflight.response.status, 'success'); + assert.ok(preflight.response.preflight); } finally { cleanup(tmp); } From 6b0a9ed65db6be1835c4a16df3d1150ce532d8c2 Mon Sep 17 00:00:00 2001 From: PatrickSys Date: Thu, 5 Nov 2026 00:24:46 +0100 Subject: [PATCH 3/4] fix: preserve optional CBC workflows within Workspine CI guards Keep XML sections and line budgets intact; document the registered integrations CLI command. Targeted and full 31-file CI suite pass; fresh consumer integrations show is off by default. Agent: chat-on-steroids-core/86f31bc2-34b2-4051-974b-26abc00b3053 --- distilled/workflows/execute.md | 5 +---- distilled/workflows/plan.md | 9 +-------- docs/USER-GUIDE.md | 1 + 3 files changed, 3 insertions(+), 12 deletions(-) diff --git a/distilled/workflows/execute.md b/distilled/workflows/execute.md index d49d54e..e281a2e 100644 --- a/distilled/workflows/execute.md +++ b/distilled/workflows/execute.md @@ -67,7 +67,6 @@ Plans to execute: Wave 2: {plan-03-NAME.md} Plans already complete (have SUMMARY): {list} ``` - Confirm with the user before proceeding if any existing SUMMARYs look stale or incomplete. ### Execute Wave by Wave For each wave in ascending order: @@ -102,7 +101,7 @@ For each task in the plan, follow this loop: 7. Record task completion in your working notes and final SUMMARY.md. ``` -Same optional bridge as `/work-plan`: enable with `integrations.codebaseContext.enabled: true` after installing [codebase-context](https://github.com/PatrickSys/codebase-context). Artifacts live under `.work/integrations/codebase-context/`. +Same opt-in bridge as /work-plan; install Codebase Context and set integrations.codebaseContext.enabled: true. Receipts live under .work/integrations/codebase-context/. ### Frontmatter And Task Semantics The executor consumes the plan schema defined by `/work-plan`: @@ -204,9 +203,7 @@ Git rules: Reality rarely matches the plan perfectly. Handle deviations with these rules in priority order: - ### Structured mismatch taxonomy - All execution-time deltas must be classified as one of: - `factual_discovery` - the repo/runtime reality differs from the plan in a concrete, local way (wrong path, stale API shape, moved module, outdated dependency assumption) - `intent_scope_change` - the requested outcome or scope needs to change diff --git a/distilled/workflows/plan.md b/distilled/workflows/plan.md index b1e83fe..a8e2721 100644 --- a/distilled/workflows/plan.md +++ b/distilled/workflows/plan.md @@ -65,7 +65,6 @@ Before planning, acknowledge what is locked: - Patterns from previous phases - match existing conventions. Do not introduce new patterns without cause. - Deferred items - items marked v2, nice-to-have, or out of scope in SPEC.md or APPROACH.md. Do not plan for them. - Cross-check: if SPEC.md and APPROACH.md disagree on whether an item is must-have or deferred, stop and ask the user before planning. Do not silently adopt one classification over the other. - If you need to challenge a locked decision: stop, ask the developer, and document the new decision explicitly. @@ -97,11 +96,7 @@ Use these sections: Quality gate: do not proceed to goal-backward planning if you have unresolved uncertainties about the implementation approach. If research was skipped (skip conditions above apply), document the skip reason in the plan Notes section so the plan checker can verify the skip was justified. -Optional bridge to the [codebase-context](https://github.com/PatrickSys/codebase-context) CLI when it is installed and enabled in \`.work/config.json\` (\`integrations.codebaseContext.enabled: true\`). Default is off; when disabled or the CLI is missing, skip this block with no behavior change. \`plan-context\` is a Workspine helper name (not a codebase-context subcommand); it runs \`codebase-context patterns\` (MCP \`get_team_patterns\`) and per-file \`codebase-context search --intent explore\` (MCP \`search_codebase\`). -After the planned write set is known (repo-relative paths the tasks will touch), run: -\`node .work/bin/gsdd.mjs integrations codebase-context plan-context --files --plan --json\` -Read \`.work/integrations/codebase-context/plan-context.json\` when \`status\` is \`ok\` and fold relevant team patterns into planning context. When \`status\` is \`disabled\` or \`unavailable\`, continue without codebase-context. -Enable once per repo with \`node .work/bin/gsdd.mjs integrations codebase-context enable\` after installing the CLI (\`npx -y codebase-context\` or a local checkout). +Optional (default off): once integrations.codebaseContext.enabled is true and the CLI is installed, after identifying the planned write set run node .work/bin/gsdd.mjs integrations codebase-context plan-context --files --plan --json. The Workspine helper calls Codebase Context patterns and search --intent explore; read .work/integrations/codebase-context/plan-context.json on status ok, otherwise continue unchanged. Enable using node .work/bin/gsdd.mjs integrations codebase-context enable; see docs/USER-GUIDE.md for setup. ### When This Runs @@ -279,9 +274,7 @@ Task type semantics: - `type="checkpoint:review"` - executor stops for explicit review before continuing If any task uses `checkpoint:*`, the plan frontmatter must set `autonomous: false`. - ### Specificity Rules - | Too Vague | Just Right | |-------------|-------------| | "Set up the database" | "Create the user schema, wire it into the repository layer, then run `npm test -- --runInBand tests/user-schema.test.ts`" | diff --git a/docs/USER-GUIDE.md b/docs/USER-GUIDE.md index 989dcd8..9ebac30 100644 --- a/docs/USER-GUIDE.md +++ b/docs/USER-GUIDE.md @@ -346,6 +346,7 @@ Details worth knowing before you script it: | `npx -y workspine journey` | Inspect journey receipts | | `npx -y workspine git-identity` | Inspect Git identity and root | | `npx -y workspine phase-status` | Inspect phase status | +| `npx -y workspine integrations show --json` | Inspect optional local integrations; Codebase Context is disabled unless enabled explicitly | | `npx -y workspine help` | Show the concise first-use and core-command summary; this guide owns the detailed command reference | If `workspine` is globally installed, you can use the shorter `gsdd ...` form for the same commands. Generated workflow helper calls do not use the global binary; they run through `node .work/bin/gsdd.mjs ...` from the repo root. The `gsdd` binary alias is removed at the next minor release, so new scripts and CI should call `workspine ...` directly. From 63dc9c7faf90148ced266c304f033b948790260e Mon Sep 17 00:00:00 2001 From: PatrickSys Date: Thu, 5 Nov 2026 09:37:23 +0100 Subject: [PATCH 4/4] docs: keep optional CBC integration guidance readable and version-accurate Clarify unreleased CLI availability, preserve XML and workflow budgets, and retain project-local command receipts. Targeted tests 1201 pass, 4 skip; full 31-file CI before final docs copy passed. Agent: chat-on-steroids-core/86f31bc2-34b2-4051-974b-26abc00b3053 --- distilled/workflows/execute.md | 2 +- distilled/workflows/plan.md | 2 +- docs/USER-GUIDE.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/distilled/workflows/execute.md b/distilled/workflows/execute.md index e281a2e..5dc7c47 100644 --- a/distilled/workflows/execute.md +++ b/distilled/workflows/execute.md @@ -101,7 +101,7 @@ For each task in the plan, follow this loop: 7. Record task completion in your working notes and final SUMMARY.md. ``` -Same opt-in bridge as /work-plan; install Codebase Context and set integrations.codebaseContext.enabled: true. Receipts live under .work/integrations/codebase-context/. +Same opt-in bridge as `/work-plan`: install Codebase Context and enable `integrations.codebaseContext.enabled`. Receipts: `.work/integrations/codebase-context/`. ### Frontmatter And Task Semantics The executor consumes the plan schema defined by `/work-plan`: diff --git a/distilled/workflows/plan.md b/distilled/workflows/plan.md index a8e2721..1c18e48 100644 --- a/distilled/workflows/plan.md +++ b/distilled/workflows/plan.md @@ -96,7 +96,7 @@ Use these sections: Quality gate: do not proceed to goal-backward planning if you have unresolved uncertainties about the implementation approach. If research was skipped (skip conditions above apply), document the skip reason in the plan Notes section so the plan checker can verify the skip was justified. -Optional (default off): once integrations.codebaseContext.enabled is true and the CLI is installed, after identifying the planned write set run node .work/bin/gsdd.mjs integrations codebase-context plan-context --files --plan --json. The Workspine helper calls Codebase Context patterns and search --intent explore; read .work/integrations/codebase-context/plan-context.json on status ok, otherwise continue unchanged. Enable using node .work/bin/gsdd.mjs integrations codebase-context enable; see docs/USER-GUIDE.md for setup. +Optional, off by default: after selecting the write set and enabling `integrations.codebaseContext.enabled` with the CLI installed, run `node .work/bin/gsdd.mjs integrations codebase-context plan-context --files --plan --json`. The helper uses Codebase Context patterns and explore search. On `status: ok`, read `.work/integrations/codebase-context/plan-context.json`; otherwise continue unchanged. Setup: `docs/USER-GUIDE.md`. ### When This Runs diff --git a/docs/USER-GUIDE.md b/docs/USER-GUIDE.md index 9ebac30..49b6cde 100644 --- a/docs/USER-GUIDE.md +++ b/docs/USER-GUIDE.md @@ -346,7 +346,7 @@ Details worth knowing before you script it: | `npx -y workspine journey` | Inspect journey receipts | | `npx -y workspine git-identity` | Inspect Git identity and root | | `npx -y workspine phase-status` | Inspect phase status | -| `npx -y workspine integrations show --json` | Inspect optional local integrations; Codebase Context is disabled unless enabled explicitly | +| `npx -y workspine integrations show --json` | **After this integration is released** (not in current npm `latest`): inspect optional integrations. Until then use `node bin/gsdd.mjs integrations show --json` from a checkout of this branch, or `node .work/bin/gsdd.mjs integrations show --json` in a project set up from this build. Codebase Context stays disabled by default. | | `npx -y workspine help` | Show the concise first-use and core-command summary; this guide owns the detailed command reference | If `workspine` is globally installed, you can use the shorter `gsdd ...` form for the same commands. Generated workflow helper calls do not use the global binary; they run through `node .work/bin/gsdd.mjs ...` from the repo root. The `gsdd` binary alias is removed at the next minor release, so new scripts and CI should call `workspine ...` directly.