Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
3 changes: 3 additions & 0 deletions bin/gsdd.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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({ getCwd: () => INIT_CONTEXT.cwd });

const cmdUpdate = (...updateArgs) => {
if (updateArgs.includes('--global') || updateArgs.includes('-g')) {
Expand Down Expand Up @@ -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
Expand Down
14 changes: 12 additions & 2 deletions bin/lib/config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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.',
Expand Down Expand Up @@ -94,7 +97,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 = {
Expand All @@ -103,14 +106,19 @@ function buildConfigDefaults({ autoAdvance = false, rigorProfile }) {
...cost,
commitDocs: true,
gitProtocol: { ...DEFAULT_GIT_PROTOCOL },
integrations,
initVersion: 'v1.1',
};
if (autoAdvance) config.autoAdvance = true;
return config;
}

export function buildDefaultConfig({ autoAdvance = false } = {}) {
return buildConfigDefaults({ autoAdvance, rigorProfile: 'high' });
return buildConfigDefaults({ autoAdvance, rigorProfile: 'high', integrations: defaultIntegrations() });
}

function defaultIntegrations() {
return mergeIntegrations({});
}

function buildExistingConfigDefaults(existing = {}, { autoAdvance = false } = {}) {
Expand Down Expand Up @@ -139,6 +147,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.`);
Expand Down Expand Up @@ -166,6 +175,7 @@ function loadConfigForMutation(cwd = process.cwd()) {
...defaults,
...existing,
workflow: { ...defaults.workflow, ...(existing.workflow ?? {}) },
integrations: mergeIntegrations(existing.integrations),
},
};
} catch (e) {
Expand Down
10 changes: 10 additions & 0 deletions bin/lib/init-runtime.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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|<step> <level>]',
integrations: 'Usage: workspine integrations [show] | codebase-context <plan-context|edit-preflight|enable|disable|show> [flags]',
}[command];
if (!usage) return null;

Expand Down Expand Up @@ -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);
}

Expand Down
120 changes: 120 additions & 0 deletions bin/lib/integrations-cli.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import {
cmdCodebaseContextSubcommand,
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 { 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) {
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;
}

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 <plan-context|edit-preflight|enable|disable|show> [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 = validateIntegrationsShape(integrationArgs);
if (grammarError) {
console.error(grammarError);
process.exitCode = 1;
return;
}

const json = integrationArgs.includes('--json');
const workspaceRoot = resolveIntegrationsRoot(integrationArgs, resolveCwd());
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}`);
console.log(' bridge: Workspine plan-context → CC `patterns` + `search --intent explore`; edit-preflight → `search --intent edit`');
}
return;
}

if (first === 'codebase-context') {
const flagArgs = subcommandFlagArgs(workspaceRoot.flagArgs);
if (json && !flagArgs.includes('--json')) flagArgs.push('--json');
return cmdCodebaseContextSubcommand(second, flagArgs, {
cwd: workspaceRoot.workspaceRoot,
json,
runner: defaultRunner,
});
}

console.error('Usage: workspine integrations [show] | codebase-context <plan-context|edit-preflight|enable|disable|show> [flags]');
process.exitCode = 1;
};
}
64 changes: 64 additions & 0 deletions bin/lib/integrations-config.mjs
Original file line number Diff line number Diff line change
@@ -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;
}
Loading
Loading