@michaelxwb/code-flow 0.6.2 → 0.6.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/package.json +1 -1
- package/src/adapters/claude/commands/cf-init.md +1 -0
- package/src/adapters/claude/commands/cf-spec.md +7 -3
- package/src/adapters/claude/commands/cf-task/archive.md +5 -5
- package/src/adapters/claude/commands/cf-task/block.md +15 -9
- package/src/adapters/claude/commands/cf-task/graph.md +8 -1
- package/src/adapters/claude/commands/cf-task/note.md +1 -4
- package/src/adapters/claude/commands/cf-task/plan.md +2 -2
- package/src/adapters/claude/commands/cf-task/start.md +96 -25
- package/src/adapters/claude/commands/cf-task/verify-e2e.md +8 -9
- package/src/adapters/claude/commands/cf-validate.md +15 -13
- package/src/adapters/codex/skills/cf-init/SKILL.md +1 -0
- package/src/adapters/codex/skills/cf-spec/SKILL.md +6 -2
- package/src/adapters/codex/skills/cf-task-archive/SKILL.md +5 -5
- package/src/adapters/codex/skills/cf-task-block/SKILL.md +15 -9
- package/src/adapters/codex/skills/cf-task-graph/SKILL.md +8 -1
- package/src/adapters/codex/skills/cf-task-note/SKILL.md +1 -4
- package/src/adapters/codex/skills/cf-task-plan/SKILL.md +2 -2
- package/src/adapters/codex/skills/cf-task-start/SKILL.md +96 -25
- package/src/adapters/codex/skills/cf-task-verify-e2e/SKILL.md +106 -0
- package/src/adapters/codex/skills/cf-validate/SKILL.md +15 -13
- package/src/adapters/costrict/commands/cf-init.md +1 -0
- package/src/adapters/costrict/commands/cf-spec.md +7 -3
- package/src/adapters/costrict/commands/cf-task/archive.md +5 -5
- package/src/adapters/costrict/commands/cf-task/block.md +15 -9
- package/src/adapters/costrict/commands/cf-task/graph.md +8 -1
- package/src/adapters/costrict/commands/cf-task/note.md +1 -4
- package/src/adapters/costrict/commands/cf-task/plan.md +2 -2
- package/src/adapters/costrict/commands/cf-task/start.md +96 -25
- package/src/adapters/costrict/commands/cf-task/verify-e2e.md +8 -9
- package/src/adapters/costrict/commands/cf-validate.md +15 -13
- package/src/adapters/opencode/commands/cf-init.md +9 -4
- package/src/adapters/opencode/commands/cf-spec.md +6 -2
- package/src/adapters/opencode/commands/cf-task/archive.md +5 -5
- package/src/adapters/opencode/commands/cf-task/block.md +15 -9
- package/src/adapters/opencode/commands/cf-task/graph.md +8 -1
- package/src/adapters/opencode/commands/cf-task/note.md +1 -4
- package/src/adapters/opencode/commands/cf-task/plan.md +2 -2
- package/src/adapters/opencode/commands/cf-task/start.md +96 -25
- package/src/adapters/opencode/commands/cf-task/verify-e2e.md +8 -9
- package/src/adapters/opencode/commands/cf-validate.md +15 -13
- package/src/adapters/opencode/opencode.json +1 -2
- package/src/adapters/opencode/plugins/code-flow/index.js +176 -94
- package/src/adapters/opencode/plugins/code-flow/package.json +1 -1
- package/src/cli.js +228 -64
- package/src/core/code-flow/config.yml +2 -1
- package/src/core/code-flow/scripts/cf_acceptance_evidence.py +100 -0
- package/src/core/code-flow/scripts/cf_acceptance_manifest.py +137 -42
- package/src/core/code-flow/scripts/cf_acceptance_runner.py +123 -38
- package/src/core/code-flow/scripts/cf_acceptance_schema.py +83 -0
- package/src/core/code-flow/scripts/cf_core.py +34 -1
- package/src/core/code-flow/scripts/cf_exec_base.py +112 -0
- package/src/core/code-flow/scripts/cf_post_hook.py +5 -4
- package/src/core/code-flow/scripts/cf_pre_tool_hook.py +22 -7
- package/src/core/code-flow/scripts/cf_scan.py +8 -6
- package/src/core/code-flow/scripts/cf_spec_context.py +197 -63
- package/src/core/code-flow/scripts/cf_spec_gate.py +26 -4
- package/src/core/code-flow/scripts/cf_spec_resolver.py +7 -2
- package/src/core/code-flow/scripts/cf_spec_router.py +3 -1
- package/src/core/code-flow/scripts/cf_spec_session.py +3 -2
- package/src/core/code-flow/scripts/cf_spec_verify.py +42 -31
- package/src/core/code-flow/scripts/cf_stats.py +41 -9
- package/src/core/code-flow/scripts/cf_stop_hook.py +128 -61
- package/src/core/code-flow/scripts/cf_task_index.py +148 -0
- package/src/core/code-flow/scripts/cf_task_parallel.py +385 -0
- package/src/core/code-flow/scripts/cf_task_runtime.py +98 -29
- package/src/core/code-flow/scripts/cf_task_state.py +3 -0
- package/src/core/code-flow/scripts/cf_task_workflow.py +107 -0
- package/src/core/code-flow/scripts/cf_user_prompt_hook.py +9 -5
- package/src/core/code-flow/scripts/cf_validation.py +81 -0
- package/src/core/code-flow/scripts/cf_workflow_service.py +366 -0
- package/src/core/code-flow/scripts/cf_workflow_transaction.py +96 -0
- package/src/core/code-flow/validation.yml +5 -0
package/src/cli.js
CHANGED
|
@@ -10,7 +10,7 @@ const pkg = require('../package.json');
|
|
|
10
10
|
const specWorkflowMigration = require('./migrate/spec-workflow');
|
|
11
11
|
|
|
12
12
|
const usage = [
|
|
13
|
-
'Usage: code-flow init [--force] [--platform=<claude|codex|costrict|opencode>]',
|
|
13
|
+
'Usage: code-flow init [--force] [--platform=<claude|codex|costrict|opencode>] [--backup-dir=<path>]',
|
|
14
14
|
' code-flow migrate --spec-workflow <--dry-run|--prepare|--apply --plan <path>|--rollback <id>>',
|
|
15
15
|
' code-flow -v | --version',
|
|
16
16
|
' code-flow -h | --help'
|
|
@@ -83,6 +83,43 @@ function compareVersions(a, b) {
|
|
|
83
83
|
return 0;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
function modeFor(installed, force) {
|
|
87
|
+
if (force) return 'force';
|
|
88
|
+
if (!installed) return 'fresh';
|
|
89
|
+
if (compareVersions(installed, pkg.version) < 0) return 'upgrade';
|
|
90
|
+
return 'current';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Per-platform install records live beside the legacy global .version so one
|
|
94
|
+
// platform's upgrade never masks another platform's staleness.
|
|
95
|
+
function readAdapterVersions(cwd) {
|
|
96
|
+
const file = path.join(cwd, '.code-flow', '.adapter-versions.json');
|
|
97
|
+
try {
|
|
98
|
+
const data = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
99
|
+
if (data && typeof data === 'object' && data.adapters && typeof data.adapters === 'object') {
|
|
100
|
+
return { core: typeof data.core === 'string' ? data.core : null, adapters: data.adapters };
|
|
101
|
+
}
|
|
102
|
+
} catch (_) { /* missing or corrupt: fall through */ }
|
|
103
|
+
return { core: null, adapters: {} };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function writeAdapterVersions(cwd, versions) {
|
|
107
|
+
const file = path.join(cwd, '.code-flow', '.adapter-versions.json');
|
|
108
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
109
|
+
fs.writeFileSync(file, JSON.stringify({ core: versions.core, adapters: versions.adapters }, null, 2) + '\n');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Marker dirs proving a platform was installed before per-platform tracking.
|
|
113
|
+
function platformFilesExist(cwd, platform) {
|
|
114
|
+
const markers = {
|
|
115
|
+
claude: ['.claude/commands'],
|
|
116
|
+
codex: ['.agents/skills'],
|
|
117
|
+
costrict: ['.costrict/commands'],
|
|
118
|
+
opencode: ['.opencode/commands'],
|
|
119
|
+
};
|
|
120
|
+
return (markers[platform] || []).some(rel => fs.existsSync(path.join(cwd, rel)));
|
|
121
|
+
}
|
|
122
|
+
|
|
86
123
|
// --- Merge functions ---
|
|
87
124
|
|
|
88
125
|
// Replace fenced code blocks with same-length whitespace so byte offsets in
|
|
@@ -200,6 +237,56 @@ function mergeSettingsJson(srcFile, destFile) {
|
|
|
200
237
|
return added;
|
|
201
238
|
}
|
|
202
239
|
|
|
240
|
+
// OpenCode v2 merge: migrate the v1 `plugin` key to `plugins`, drop the
|
|
241
|
+
// auto-discovered local path (`.opencode/plugins/*` loads without config),
|
|
242
|
+
// and union template `plugins` entries without overwriting user content.
|
|
243
|
+
function mergeOpencodeJson(srcFile, destFile) {
|
|
244
|
+
const src = JSON.parse(fs.readFileSync(srcFile, 'utf8'));
|
|
245
|
+
const dest = JSON.parse(fs.readFileSync(destFile, 'utf8'));
|
|
246
|
+
const added = [];
|
|
247
|
+
const isLocalEntry = (e) => {
|
|
248
|
+
const p = typeof e === 'string' ? e : (e && e.package);
|
|
249
|
+
return typeof p === 'string' && /(^|\/)\.opencode\/plugins\//.test(p);
|
|
250
|
+
};
|
|
251
|
+
if (Array.isArray(dest.plugin)) {
|
|
252
|
+
const carried = dest.plugin.filter((e) => !isLocalEntry(e));
|
|
253
|
+
delete dest.plugin;
|
|
254
|
+
added.push(`removed legacy 'plugin' key (v1 → v2)`);
|
|
255
|
+
if (carried.length > 0 && !Array.isArray(dest.plugins)) dest.plugins = carried;
|
|
256
|
+
} else if ('plugin' in dest) {
|
|
257
|
+
delete dest.plugin;
|
|
258
|
+
added.push(`removed legacy 'plugin' key (v1 → v2)`);
|
|
259
|
+
}
|
|
260
|
+
if (Array.isArray(dest.plugins)) {
|
|
261
|
+
const before = dest.plugins.length;
|
|
262
|
+
dest.plugins = dest.plugins.filter((e) => !isLocalEntry(e));
|
|
263
|
+
if (dest.plugins.length !== before) added.push('removed auto-discovered local plugin entry');
|
|
264
|
+
}
|
|
265
|
+
if (Array.isArray(src.plugins)) {
|
|
266
|
+
if (!Array.isArray(dest.plugins)) dest.plugins = [];
|
|
267
|
+
const seen = new Set(dest.plugins.map((e) => JSON.stringify(e)));
|
|
268
|
+
for (const entry of src.plugins) {
|
|
269
|
+
const key = JSON.stringify(entry);
|
|
270
|
+
if (!seen.has(key)) {
|
|
271
|
+
dest.plugins.push(entry);
|
|
272
|
+
seen.add(key);
|
|
273
|
+
added.push(`plugins +${typeof entry === 'string' ? entry : key}`.slice(0, 80));
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
for (const key of Object.keys(src)) {
|
|
278
|
+
if (key === 'plugins' || key === 'plugin') continue;
|
|
279
|
+
if (!(key in dest)) {
|
|
280
|
+
dest[key] = src[key];
|
|
281
|
+
added.push(key);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (added.length > 0) {
|
|
285
|
+
fs.writeFileSync(destFile, JSON.stringify(dest, null, 2) + '\n');
|
|
286
|
+
}
|
|
287
|
+
return added;
|
|
288
|
+
}
|
|
289
|
+
|
|
203
290
|
function mergeConfigYml(srcFile, destFile) {
|
|
204
291
|
const srcText = fs.readFileSync(srcFile, 'utf8');
|
|
205
292
|
const destText = fs.readFileSync(destFile, 'utf8');
|
|
@@ -342,6 +429,53 @@ function copyFileIfMissing(srcFile, destFile) {
|
|
|
342
429
|
fs.copyFileSync(srcFile, destFile);
|
|
343
430
|
}
|
|
344
431
|
|
|
432
|
+
function removeLegacyClaudeSkills(cwd, removed, opts) {
|
|
433
|
+
// 0.1-era code-flow deployed its own skills (cf-*.md) under .claude/skills/.
|
|
434
|
+
// Only those historically managed names may be touched; everything else is
|
|
435
|
+
// user-owned and must survive init. Managed entries are backed up before
|
|
436
|
+
// removal. The legacy dir belongs to the claude domain: other platforms
|
|
437
|
+
// must never touch it.
|
|
438
|
+
const options = opts || {};
|
|
439
|
+
if (options.platform && options.platform !== 'claude') return;
|
|
440
|
+
const legacySkills = path.join(cwd, '.claude', 'skills');
|
|
441
|
+
if (!fs.existsSync(legacySkills)) return;
|
|
442
|
+
|
|
443
|
+
let entries;
|
|
444
|
+
try {
|
|
445
|
+
entries = fs.readdirSync(legacySkills, { withFileTypes: true });
|
|
446
|
+
} catch (error) {
|
|
447
|
+
const code = error && error.code ? ` (${error.code})` : '';
|
|
448
|
+
process.stderr.write(`Warning: failed to list deprecated .claude/skills/${code}; left untouched. Remove it manually.\n`);
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
452
|
+
const backupRoot = options.backupDir
|
|
453
|
+
? path.resolve(cwd, options.backupDir)
|
|
454
|
+
: path.join(cwd, '.code-flow', 'backups', stamp);
|
|
455
|
+
for (const entry of entries) {
|
|
456
|
+
if (!MANAGED_LEGACY_SKILL_NAMES.has(entry.name)) continue; // user-owned: keep
|
|
457
|
+
const src = path.join(legacySkills, entry.name);
|
|
458
|
+
const rel = path.join('.claude', 'skills', entry.name);
|
|
459
|
+
try {
|
|
460
|
+
const dest = path.join(backupRoot, rel);
|
|
461
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
462
|
+
if (entry.isDirectory()) {
|
|
463
|
+
copyDirRecursive(src, dest, true);
|
|
464
|
+
} else if (entry.isFile()) {
|
|
465
|
+
fs.copyFileSync(src, dest);
|
|
466
|
+
} else {
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
fs.rmSync(src, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
|
|
470
|
+
removed.push(`${rel} (backup: ${path.relative(cwd, dest)})`);
|
|
471
|
+
} catch (error) {
|
|
472
|
+
const code = error && error.code ? ` (${error.code})` : '';
|
|
473
|
+
const message = error && error.message ? error.message : String(error);
|
|
474
|
+
process.stderr.write(`Warning: failed to migrate deprecated ${rel}${code}: ${message}\n`);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
345
479
|
// Files renamed in 0.x → 1.x that may linger in upgraded projects.
|
|
346
480
|
// Each entry: { path: relative-to-cwd, replacedBy: hint string for the warning }.
|
|
347
481
|
const ORPHAN_FILES = [
|
|
@@ -367,35 +501,30 @@ function removeOrphanFiles(cwd, removed) {
|
|
|
367
501
|
}
|
|
368
502
|
}
|
|
369
503
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
});
|
|
381
|
-
if (!fs.existsSync(legacySkills)) {
|
|
382
|
-
removed.push('.claude/skills/');
|
|
383
|
-
return;
|
|
384
|
-
}
|
|
385
|
-
} catch (error) {
|
|
386
|
-
const code = error && error.code ? ` (${error.code})` : '';
|
|
387
|
-
const message = error && error.message ? error.message : String(error);
|
|
388
|
-
process.stderr.write(`Warning: failed to remove deprecated .claude/skills/${code}: ${message}\n`);
|
|
389
|
-
return;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
process.stderr.write('Warning: failed to remove deprecated .claude/skills/. Remove it manually.\n');
|
|
393
|
-
}
|
|
504
|
+
// Skill entries historically deployed by code-flow 0.x under .claude/skills/.
|
|
505
|
+
// Only these names are managed legacy content; any other entry is user-owned.
|
|
506
|
+
const MANAGED_LEGACY_SKILL_NAMES = new Set([
|
|
507
|
+
'cf-init.md',
|
|
508
|
+
'cf-inject.md',
|
|
509
|
+
'cf-learn.md',
|
|
510
|
+
'cf-scan.md',
|
|
511
|
+
'cf-stats.md',
|
|
512
|
+
'cf-validate.md',
|
|
513
|
+
]);
|
|
394
514
|
|
|
395
515
|
// --- pyyaml dependency: probe first, fall back through PEP 668 strategies ---
|
|
396
516
|
|
|
397
|
-
|
|
398
|
-
|
|
517
|
+
// --- pyyaml dependency: probe first, fall back through PEP 668 strategies ---
|
|
518
|
+
// Bounded waits: each attempt carries its own timeout inside a total budget,
|
|
519
|
+
// with progress on stderr. spawnFn/logFn/nowFn are injectable for tests.
|
|
520
|
+
const PIP_PER_ATTEMPT_MS = 60000;
|
|
521
|
+
const PIP_TOTAL_MS = 120000;
|
|
522
|
+
|
|
523
|
+
function ensurePyYaml(spawnFn, logFn, nowFn) {
|
|
524
|
+
const spawn = spawnFn || spawnSync;
|
|
525
|
+
const log = logFn || ((message) => process.stderr.write(message));
|
|
526
|
+
const now = nowFn || Date.now;
|
|
527
|
+
const probe = spawn('python3', ['-c', 'import yaml'], { stdio: 'ignore' });
|
|
399
528
|
if (!probe.error && probe.status === 0) return; // already installed
|
|
400
529
|
|
|
401
530
|
const attempts = [
|
|
@@ -403,11 +532,18 @@ function ensurePyYaml() {
|
|
|
403
532
|
['-m', 'pip', 'install', 'pyyaml'],
|
|
404
533
|
['-m', 'pip', 'install', '--user', '--break-system-packages', 'pyyaml'],
|
|
405
534
|
];
|
|
406
|
-
|
|
407
|
-
|
|
535
|
+
const started = now();
|
|
536
|
+
for (let i = 0; i < attempts.length; i++) {
|
|
537
|
+
const remaining = PIP_TOTAL_MS - (now() - started);
|
|
538
|
+
if (remaining <= 0) break;
|
|
539
|
+
log(`Installing pyyaml (attempt ${i + 1}/${attempts.length})...\n`);
|
|
540
|
+
const result = spawn('python3', attempts[i], {
|
|
541
|
+
stdio: 'ignore',
|
|
542
|
+
timeout: Math.min(PIP_PER_ATTEMPT_MS, remaining),
|
|
543
|
+
});
|
|
408
544
|
if (!result.error && result.status === 0) return;
|
|
409
545
|
}
|
|
410
|
-
|
|
546
|
+
log(
|
|
411
547
|
'Warning: pyyaml install failed. Run manually:\n' +
|
|
412
548
|
' python3 -m pip install --user pyyaml\n' +
|
|
413
549
|
' # or, on PEP 668 systems:\n' +
|
|
@@ -468,10 +604,21 @@ function parsePlatform(args) {
|
|
|
468
604
|
return null;
|
|
469
605
|
}
|
|
470
606
|
|
|
607
|
+
function parseStringFlag(args, name) {
|
|
608
|
+
const prefix = `--${name}=`;
|
|
609
|
+
for (const arg of args) {
|
|
610
|
+
if (arg.startsWith(prefix)) return arg.slice(prefix.length);
|
|
611
|
+
}
|
|
612
|
+
const idx = args.indexOf(`--${name}`);
|
|
613
|
+
if (idx !== -1) return args[idx + 1] || '';
|
|
614
|
+
return '';
|
|
615
|
+
}
|
|
616
|
+
|
|
471
617
|
// --- Main init ---
|
|
472
618
|
|
|
473
|
-
function runInit(force, platform) {
|
|
619
|
+
function runInit(force, platform, opts) {
|
|
474
620
|
const cwd = process.cwd();
|
|
621
|
+
const options = opts || {};
|
|
475
622
|
const installedVersion = readInstalledVersion(cwd);
|
|
476
623
|
if (installedVersion && compareVersions(installedVersion, '0.4.2') >= 0 && compareVersions(installedVersion, '0.6.0') < 0) {
|
|
477
624
|
process.stdout.write(`${JSON.stringify({ status: 'migration_required', version: installedVersion, command: 'code-flow migrate --spec-workflow --dry-run' })}\n`);
|
|
@@ -483,16 +630,28 @@ function runInit(force, platform) {
|
|
|
483
630
|
const coreDir = path.join(baseDir, 'core');
|
|
484
631
|
const adaptersDir = path.join(baseDir, 'adapters');
|
|
485
632
|
|
|
486
|
-
// Determine mode
|
|
487
|
-
|
|
633
|
+
// Determine mode: global mode drives core files and messaging; each
|
|
634
|
+
// platform additionally resolves its own mode from per-platform records so
|
|
635
|
+
// upgrading one platform can never mask another platform's staleness.
|
|
636
|
+
// A global upgrade also refreshes the selected platform (previous behavior).
|
|
637
|
+
const mode = modeFor(installedVersion, force);
|
|
638
|
+
const adapterVersions = readAdapterVersions(cwd);
|
|
639
|
+
const adapterPrev = typeof adapterVersions.adapters[platform] === 'string'
|
|
640
|
+
? adapterVersions.adapters[platform]
|
|
641
|
+
: null;
|
|
642
|
+
let platformMode;
|
|
488
643
|
if (force) {
|
|
489
|
-
|
|
490
|
-
} else if (!
|
|
491
|
-
|
|
492
|
-
} else if (
|
|
493
|
-
|
|
644
|
+
platformMode = 'force';
|
|
645
|
+
} else if (!adapterPrev && !platformFilesExist(cwd, platform)) {
|
|
646
|
+
platformMode = 'fresh';
|
|
647
|
+
} else if (
|
|
648
|
+
(adapterPrev && compareVersions(adapterPrev, pkg.version) < 0) ||
|
|
649
|
+
(!adapterPrev && platformFilesExist(cwd, platform)) ||
|
|
650
|
+
mode === 'upgrade'
|
|
651
|
+
) {
|
|
652
|
+
platformMode = 'upgrade';
|
|
494
653
|
} else {
|
|
495
|
-
|
|
654
|
+
platformMode = 'current';
|
|
496
655
|
}
|
|
497
656
|
|
|
498
657
|
const created = [];
|
|
@@ -502,8 +661,10 @@ function runInit(force, platform) {
|
|
|
502
661
|
const removed = [];
|
|
503
662
|
const results = { created, updated, merged, skipped };
|
|
504
663
|
|
|
505
|
-
// Track + copy helper for directory trees
|
|
506
|
-
|
|
664
|
+
// Track + copy helper for directory trees. dirMode is resolved per area:
|
|
665
|
+
// core files follow the global mode, platform files follow platformMode.
|
|
666
|
+
const processDir = (srcDir, destDir, prefix, dirMode) => {
|
|
667
|
+
const effective = dirMode || mode;
|
|
507
668
|
if (!fs.existsSync(srcDir)) return;
|
|
508
669
|
const files = collectFiles(srcDir, '');
|
|
509
670
|
for (const rel of files) {
|
|
@@ -516,10 +677,10 @@ function runInit(force, platform) {
|
|
|
516
677
|
created.push(label);
|
|
517
678
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
518
679
|
fs.copyFileSync(src, dest);
|
|
519
|
-
} else if (
|
|
680
|
+
} else if (effective === 'force') {
|
|
520
681
|
updated.push(label);
|
|
521
682
|
fs.copyFileSync(src, dest);
|
|
522
|
-
} else if (
|
|
683
|
+
} else if (effective === 'upgrade' && cat === 'tool') {
|
|
523
684
|
updated.push(label);
|
|
524
685
|
fs.copyFileSync(src, dest);
|
|
525
686
|
} else {
|
|
@@ -529,7 +690,7 @@ function runInit(force, platform) {
|
|
|
529
690
|
};
|
|
530
691
|
|
|
531
692
|
// Process .code-flow/ (core)
|
|
532
|
-
processDir(path.join(coreDir, 'code-flow'), path.join(cwd, '.code-flow'), '.code-flow');
|
|
693
|
+
processDir(path.join(coreDir, 'code-flow'), path.join(cwd, '.code-flow'), '.code-flow', mode);
|
|
533
694
|
|
|
534
695
|
// Process Claude adapter
|
|
535
696
|
if (platform === 'claude') {
|
|
@@ -537,7 +698,7 @@ function runInit(force, platform) {
|
|
|
537
698
|
src: path.join(adaptersDir, 'claude', 'CLAUDE.md'),
|
|
538
699
|
dest: path.join(cwd, 'CLAUDE.md'),
|
|
539
700
|
label: 'CLAUDE.md',
|
|
540
|
-
mode,
|
|
701
|
+
mode: platformMode,
|
|
541
702
|
mergeFn: mergeClaudeMd,
|
|
542
703
|
toolOnUpgrade: false,
|
|
543
704
|
results,
|
|
@@ -547,14 +708,14 @@ function runInit(force, platform) {
|
|
|
547
708
|
processDir(
|
|
548
709
|
path.join(adaptersDir, 'claude', 'commands'),
|
|
549
710
|
path.join(cwd, '.claude', 'commands'),
|
|
550
|
-
'.claude/commands'
|
|
711
|
+
'.claude/commands', platformMode
|
|
551
712
|
);
|
|
552
713
|
|
|
553
714
|
installAdapterFile({
|
|
554
715
|
src: path.join(adaptersDir, 'claude', 'settings.local.json'),
|
|
555
716
|
dest: path.join(cwd, '.claude', 'settings.local.json'),
|
|
556
717
|
label: '.claude/settings.local.json',
|
|
557
|
-
mode,
|
|
718
|
+
mode: platformMode,
|
|
558
719
|
mergeFn: mergeSettingsJson,
|
|
559
720
|
toolOnUpgrade: false,
|
|
560
721
|
results,
|
|
@@ -567,7 +728,7 @@ function runInit(force, platform) {
|
|
|
567
728
|
src: path.join(adaptersDir, 'costrict', 'CLAUDE.md'),
|
|
568
729
|
dest: path.join(cwd, 'CLAUDE.md'),
|
|
569
730
|
label: 'CLAUDE.md',
|
|
570
|
-
mode,
|
|
731
|
+
mode: platformMode,
|
|
571
732
|
mergeFn: mergeClaudeMd,
|
|
572
733
|
toolOnUpgrade: false,
|
|
573
734
|
results,
|
|
@@ -577,14 +738,14 @@ function runInit(force, platform) {
|
|
|
577
738
|
processDir(
|
|
578
739
|
path.join(adaptersDir, 'costrict', 'commands'),
|
|
579
740
|
path.join(cwd, '.costrict', 'commands'),
|
|
580
|
-
'.costrict/commands'
|
|
741
|
+
'.costrict/commands', platformMode
|
|
581
742
|
);
|
|
582
743
|
|
|
583
744
|
installAdapterFile({
|
|
584
745
|
src: path.join(adaptersDir, 'costrict', 'settings.local.json'),
|
|
585
746
|
dest: path.join(cwd, '.costrict', 'settings.local.json'),
|
|
586
747
|
label: '.costrict/settings.local.json',
|
|
587
|
-
mode,
|
|
748
|
+
mode: platformMode,
|
|
588
749
|
mergeFn: mergeSettingsJson,
|
|
589
750
|
toolOnUpgrade: false,
|
|
590
751
|
results,
|
|
@@ -597,7 +758,7 @@ function runInit(force, platform) {
|
|
|
597
758
|
src: path.join(adaptersDir, 'codex', 'AGENTS.md'),
|
|
598
759
|
dest: path.join(cwd, 'AGENTS.md'),
|
|
599
760
|
label: 'AGENTS.md',
|
|
600
|
-
mode,
|
|
761
|
+
mode: platformMode,
|
|
601
762
|
mergeFn: mergeClaudeMd,
|
|
602
763
|
toolOnUpgrade: false,
|
|
603
764
|
results,
|
|
@@ -607,7 +768,7 @@ function runInit(force, platform) {
|
|
|
607
768
|
src: path.join(adaptersDir, 'codex', 'hooks.json'),
|
|
608
769
|
dest: path.join(cwd, '.codex', 'hooks.json'),
|
|
609
770
|
label: '.codex/hooks.json',
|
|
610
|
-
mode,
|
|
771
|
+
mode: platformMode,
|
|
611
772
|
mergeFn: mergeSettingsJson,
|
|
612
773
|
toolOnUpgrade: false,
|
|
613
774
|
results,
|
|
@@ -617,7 +778,7 @@ function runInit(force, platform) {
|
|
|
617
778
|
src: path.join(adaptersDir, 'codex', 'config.toml'),
|
|
618
779
|
dest: path.join(cwd, '.codex', 'config.toml'),
|
|
619
780
|
label: '.codex/config.toml',
|
|
620
|
-
mode,
|
|
781
|
+
mode: platformMode,
|
|
621
782
|
mergeFn: mergeCodexConfigToml,
|
|
622
783
|
toolOnUpgrade: false,
|
|
623
784
|
results,
|
|
@@ -627,7 +788,7 @@ function runInit(force, platform) {
|
|
|
627
788
|
processDir(
|
|
628
789
|
path.join(adaptersDir, 'codex', 'skills'),
|
|
629
790
|
path.join(cwd, '.agents', 'skills'),
|
|
630
|
-
'.agents/skills'
|
|
791
|
+
'.agents/skills', platformMode
|
|
631
792
|
);
|
|
632
793
|
}
|
|
633
794
|
|
|
@@ -637,7 +798,7 @@ function runInit(force, platform) {
|
|
|
637
798
|
src: path.join(adaptersDir, 'opencode', 'AGENTS.md'),
|
|
638
799
|
dest: path.join(cwd, 'AGENTS.md'),
|
|
639
800
|
label: 'AGENTS.md',
|
|
640
|
-
mode,
|
|
801
|
+
mode: platformMode,
|
|
641
802
|
mergeFn: mergeClaudeMd,
|
|
642
803
|
toolOnUpgrade: false,
|
|
643
804
|
results,
|
|
@@ -647,7 +808,7 @@ function runInit(force, platform) {
|
|
|
647
808
|
processDir(
|
|
648
809
|
path.join(adaptersDir, 'opencode', 'plugins'),
|
|
649
810
|
path.join(cwd, '.opencode', 'plugins'),
|
|
650
|
-
'.opencode/plugins'
|
|
811
|
+
'.opencode/plugins', platformMode
|
|
651
812
|
);
|
|
652
813
|
|
|
653
814
|
// Stamp main package version into the plugin's package.json
|
|
@@ -662,15 +823,15 @@ function runInit(force, platform) {
|
|
|
662
823
|
processDir(
|
|
663
824
|
path.join(adaptersDir, 'opencode', 'commands'),
|
|
664
825
|
path.join(cwd, '.opencode', 'commands'),
|
|
665
|
-
'.opencode/commands'
|
|
826
|
+
'.opencode/commands', platformMode
|
|
666
827
|
);
|
|
667
828
|
|
|
668
829
|
installAdapterFile({
|
|
669
830
|
src: path.join(adaptersDir, 'opencode', 'opencode.json'),
|
|
670
831
|
dest: path.join(cwd, 'opencode.json'),
|
|
671
832
|
label: 'opencode.json',
|
|
672
|
-
mode,
|
|
673
|
-
mergeFn:
|
|
833
|
+
mode: platformMode,
|
|
834
|
+
mergeFn: mergeOpencodeJson,
|
|
674
835
|
toolOnUpgrade: false,
|
|
675
836
|
results,
|
|
676
837
|
});
|
|
@@ -689,8 +850,8 @@ function runInit(force, platform) {
|
|
|
689
850
|
}
|
|
690
851
|
}
|
|
691
852
|
|
|
692
|
-
// Clean up legacy .claude/skills/
|
|
693
|
-
removeLegacyClaudeSkills(cwd, removed);
|
|
853
|
+
// Clean up legacy .claude/skills/ (claude domain only; user-owned kept, managed backed up)
|
|
854
|
+
removeLegacyClaudeSkills(cwd, removed, { platform, backupDir: options.backupDir || '' });
|
|
694
855
|
|
|
695
856
|
// Clean up renamed orphan scripts (only meaningful on upgrade)
|
|
696
857
|
if (mode === 'upgrade' || mode === 'force') {
|
|
@@ -700,13 +861,16 @@ function runInit(force, platform) {
|
|
|
700
861
|
// Install pyyaml — probe first, fall back through PEP 668 strategies
|
|
701
862
|
ensurePyYaml();
|
|
702
863
|
|
|
703
|
-
// Write version
|
|
864
|
+
// Write version (legacy global file kept for compatibility) + per-platform records
|
|
704
865
|
writeVersion(cwd, pkg.version);
|
|
866
|
+
adapterVersions.adapters[platform] = pkg.version;
|
|
867
|
+
adapterVersions.core = pkg.version;
|
|
868
|
+
writeAdapterVersions(cwd, adapterVersions);
|
|
705
869
|
|
|
706
870
|
// Output summary
|
|
707
871
|
if (mode === 'upgrade') {
|
|
708
872
|
process.stdout.write(`\ncode-flow upgraded: ${installedVersion} → ${pkg.version}\n\n`);
|
|
709
|
-
} else if (mode === 'current' && created.length === 0 && updated.length === 0 && merged.length === 0) {
|
|
873
|
+
} else if (mode === 'current' && platformMode === 'current' && created.length === 0 && updated.length === 0 && merged.length === 0) {
|
|
710
874
|
process.stdout.write(`code-flow v${pkg.version} already up to date.\n`);
|
|
711
875
|
process.exit(0);
|
|
712
876
|
} else if (mode === 'force') {
|
|
@@ -841,7 +1005,7 @@ if (args[0] === 'init') {
|
|
|
841
1005
|
if (platform !== 'claude' && platform !== 'codex' && platform !== 'costrict' && platform !== 'opencode') {
|
|
842
1006
|
fail(`Error: --platform must be "claude", "codex", "costrict", or "opencode", got "${platform}".`);
|
|
843
1007
|
}
|
|
844
|
-
runInit(force, platform);
|
|
1008
|
+
runInit(force, platform, { backupDir: parseStringFlag(args, 'backup-dir') });
|
|
845
1009
|
}
|
|
846
1010
|
|
|
847
1011
|
if (args[0] === 'migrate') {
|
|
@@ -24,7 +24,8 @@ budget:
|
|
|
24
24
|
quality_loop:
|
|
25
25
|
enabled: true
|
|
26
26
|
post_check: true # PostToolUse 违规反馈(只提示不阻断)
|
|
27
|
-
stop_check: true # Stop 收尾按 validation.yml
|
|
27
|
+
stop_check: true # Stop 收尾按 validation.yml 校验(heavy 项跳过)
|
|
28
|
+
finish_check: true # 任务 finish 时全量执行 validation.yml(含 heavy,每任务一次)
|
|
28
29
|
correction_capture: true # 纠正句式采集(喂 cf-learn 候选)
|
|
29
30
|
# 注入时对 spec 内容做保守无损压缩(去行尾空白、折叠多空行、剥 HTML 注释、去重 bullet)
|
|
30
31
|
# 缺省视为 true;仅显式 false 关闭
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Append execution history without erasing the user's acceptance contract."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import re
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Mapping, Sequence
|
|
9
|
+
from uuid import uuid4
|
|
10
|
+
|
|
11
|
+
STATES = frozenset(("planned", "pending", "tbd", "verified", "unverified", "failed", "incomplete", "not_configured", "e2e_deferred", "manual_pending"))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def scenario_line(line: str, scenario_id: str) -> bool:
|
|
15
|
+
return bool(re.match(rf"^\s*(?:\|\s*|[-*]\s+){re.escape(scenario_id)}(?=\s*[:|])", line))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def line_status(line: str, scenario_id: str) -> str:
|
|
19
|
+
if not scenario_line(line, scenario_id):
|
|
20
|
+
return ""
|
|
21
|
+
if line.lstrip().startswith("|"):
|
|
22
|
+
cells = [cell.strip().lower() for cell in line.strip().strip("|").split("|")]
|
|
23
|
+
return next((cell for cell in reversed(cells) if cell in STATES), "")
|
|
24
|
+
match = re.match(r"^\s*[-*]\s+[^:]+:\s*([a-z0-9_]+)\b", line, re.I)
|
|
25
|
+
return match.group(1).lower() if match and match.group(1).lower() in STATES else ""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _status_line(line: str, scenario_id: str, status: str) -> str:
|
|
29
|
+
if not scenario_line(line, scenario_id):
|
|
30
|
+
return line
|
|
31
|
+
if line.lstrip().startswith("|"):
|
|
32
|
+
cells = line.split("|")
|
|
33
|
+
for index in range(len(cells) - 1, 0, -1):
|
|
34
|
+
if cells[index].strip().lower() in STATES:
|
|
35
|
+
cells[index] = f" {status} "
|
|
36
|
+
return "|".join(cells)
|
|
37
|
+
return line.rstrip().rstrip("|") + f"| {status} |"
|
|
38
|
+
prefix = r"(^\s*[-*]\s+[^:]+:\s*)"
|
|
39
|
+
if line_status(line, scenario_id):
|
|
40
|
+
return re.sub(prefix + r"(?:" + "|".join(sorted(STATES)) + r")\b", rf"\g<1>{status}", line, count=1, flags=re.I)
|
|
41
|
+
return re.sub(prefix, rf"\g<1>{status} — ", line, count=1)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _patch_section(section: str, scenario_id: str, status: str, record: str) -> str:
|
|
45
|
+
contract = re.search(r"(?ms)^### Acceptance Contract[^\n]*\n(.*?)(?=^### |^## |\Z)", section)
|
|
46
|
+
if contract:
|
|
47
|
+
body = "\n".join(_status_line(line, scenario_id, status) for line in contract.group(1).splitlines()) + "\n"
|
|
48
|
+
section = section[:contract.start(1)] + body + section[contract.end(1):]
|
|
49
|
+
evidence = re.search(r"(?ms)^### Acceptance Evidence[^\n]*\n(.*?)(?=^### |^## |\Z)", section)
|
|
50
|
+
if evidence:
|
|
51
|
+
body = evidence.group(1)
|
|
52
|
+
if record not in body.splitlines():
|
|
53
|
+
body = body.rstrip() + "\n" + record + "\n\n"
|
|
54
|
+
section = section[:evidence.start(1)] + body + section[evidence.end(1):]
|
|
55
|
+
else:
|
|
56
|
+
section = section.rstrip() + "\n\n### Acceptance Evidence\n" + record + "\n"
|
|
57
|
+
return section
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def sync_task_evidence(task: Path, scenario_id: str, confirmed_by: str, evidence: str, owner: str = "", status: str = "verified") -> None:
|
|
61
|
+
text = task.read_text(encoding="utf-8")
|
|
62
|
+
task_pattern = re.escape(owner) if owner else r"TASK-\d+"
|
|
63
|
+
match = re.search(rf"(?ms)^##\s+{task_pattern}:.*?(?=^## |\Z)", text)
|
|
64
|
+
if match is None:
|
|
65
|
+
raise ValueError(f"acceptance owner missing: {owner}")
|
|
66
|
+
record = f"- {scenario_id}: {status} — {evidence} (confirmed_by: {confirmed_by})"
|
|
67
|
+
section = _patch_section(match.group(0), scenario_id, status, record)
|
|
68
|
+
text = text[:match.start()] + section + text[match.end():]
|
|
69
|
+
coverage = re.search(r"(?ms)^## Acceptance Coverage[^\n]*\n(.*?)(?=^## |\Z)", text)
|
|
70
|
+
if coverage:
|
|
71
|
+
body = "\n".join(_status_line(line, scenario_id, status) for line in coverage.group(1).splitlines()) + "\n"
|
|
72
|
+
text = text[:coverage.start(1)] + body + text[coverage.end(1):]
|
|
73
|
+
from cf_spec_context import _atomic_text
|
|
74
|
+
_atomic_text(task, text)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def persist_results(path: Path, data: dict[str, object], results: Sequence[Mapping[str, object]]) -> None:
|
|
78
|
+
rows = {row["id"]: row for row in data["scenarios"]}
|
|
79
|
+
run_id, executed_at = uuid4().hex, datetime.now(timezone.utc).isoformat()
|
|
80
|
+
updates = []
|
|
81
|
+
for result in results:
|
|
82
|
+
state = str(result["status"])
|
|
83
|
+
if state in ("not_included", "manual_pending"):
|
|
84
|
+
continue
|
|
85
|
+
row = rows[result["id"]]
|
|
86
|
+
row["revision"] = int(row.get("revision", 0)) + 1
|
|
87
|
+
status = "verified" if state == "passed" else state
|
|
88
|
+
row["status"] = status
|
|
89
|
+
record = {**result, "revision": row["revision"], "run_id": run_id, "executed_at": executed_at}
|
|
90
|
+
row["evidence"] = record
|
|
91
|
+
row.setdefault("runs", []).append(record)
|
|
92
|
+
updates.append((row, record, status))
|
|
93
|
+
from cf_spec_context import _atomic_text
|
|
94
|
+
_atomic_text(path, json.dumps(data, ensure_ascii=False, indent=2) + "\n")
|
|
95
|
+
task_name = data.get("task_file")
|
|
96
|
+
if isinstance(task_name, str) and task_name:
|
|
97
|
+
task = Path(task_name) if Path(task_name).is_absolute() else path.parent / task_name
|
|
98
|
+
for row, record, status in updates:
|
|
99
|
+
detail = f"automated command {record['status']}; run_id={run_id}"
|
|
100
|
+
sync_task_evidence(task, str(row["id"]), "runner", detail, str(row.get("owner", "")), status)
|