@michaelxwb/code-flow 0.6.1 → 0.6.3
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/package.json +1 -1
- 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 +40 -0
- package/src/adapters/claude/commands/cf-task/start.md +22 -22
- package/src/adapters/claude/commands/cf-task/verify-e2e.md +110 -0
- package/src/adapters/claude/commands/cf-validate.md +12 -13
- 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-start/SKILL.md +22 -22
- package/src/adapters/codex/skills/cf-task-verify-e2e/SKILL.md +106 -0
- package/src/adapters/codex/skills/cf-validate/SKILL.md +12 -13
- 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 +40 -0
- package/src/adapters/costrict/commands/cf-task/start.md +22 -22
- package/src/adapters/costrict/commands/cf-task/verify-e2e.md +110 -0
- package/src/adapters/costrict/commands/cf-validate.md +12 -13
- 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 +40 -5
- package/src/adapters/opencode/commands/cf-task/start.md +22 -22
- package/src/adapters/opencode/commands/cf-task/verify-e2e.md +110 -0
- package/src/adapters/opencode/commands/cf-validate.md +12 -13
- package/src/adapters/opencode/plugins/code-flow/index.js +50 -28
- package/src/cli.js +177 -63
- package/src/core/code-flow/scripts/cf_acceptance_evidence.py +100 -0
- package/src/core/code-flow/scripts/cf_acceptance_manifest.py +248 -0
- package/src/core/code-flow/scripts/cf_acceptance_runner.py +192 -0
- package/src/core/code-flow/scripts/cf_acceptance_schema.py +83 -0
- package/src/core/code-flow/scripts/cf_core.py +26 -0
- package/src/core/code-flow/scripts/cf_exec_base.py +112 -0
- package/src/core/code-flow/scripts/cf_post_hook.py +2 -0
- package/src/core/code-flow/scripts/cf_pre_tool_hook.py +13 -8
- package/src/core/code-flow/scripts/cf_scan.py +8 -6
- package/src/core/code-flow/scripts/cf_spec_context.py +267 -28
- package/src/core/code-flow/scripts/cf_spec_gate.py +10 -0
- package/src/core/code-flow/scripts/cf_spec_metadata.py +60 -11
- package/src/core/code-flow/scripts/cf_spec_resolver.py +85 -9
- package/src/core/code-flow/scripts/cf_spec_router.py +104 -9
- package/src/core/code-flow/scripts/cf_spec_session.py +3 -2
- package/src/core/code-flow/scripts/cf_spec_verify.py +159 -38
- package/src/core/code-flow/scripts/cf_stats.py +41 -9
- package/src/core/code-flow/scripts/cf_stop_hook.py +100 -59
- package/src/core/code-flow/scripts/cf_task_index.py +147 -0
- package/src/core/code-flow/scripts/cf_task_runtime.py +84 -3
- 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 +11 -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/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
|
|
@@ -342,6 +379,53 @@ function copyFileIfMissing(srcFile, destFile) {
|
|
|
342
379
|
fs.copyFileSync(srcFile, destFile);
|
|
343
380
|
}
|
|
344
381
|
|
|
382
|
+
function removeLegacyClaudeSkills(cwd, removed, opts) {
|
|
383
|
+
// 0.1-era code-flow deployed its own skills (cf-*.md) under .claude/skills/.
|
|
384
|
+
// Only those historically managed names may be touched; everything else is
|
|
385
|
+
// user-owned and must survive init. Managed entries are backed up before
|
|
386
|
+
// removal. The legacy dir belongs to the claude domain: other platforms
|
|
387
|
+
// must never touch it.
|
|
388
|
+
const options = opts || {};
|
|
389
|
+
if (options.platform && options.platform !== 'claude') return;
|
|
390
|
+
const legacySkills = path.join(cwd, '.claude', 'skills');
|
|
391
|
+
if (!fs.existsSync(legacySkills)) return;
|
|
392
|
+
|
|
393
|
+
let entries;
|
|
394
|
+
try {
|
|
395
|
+
entries = fs.readdirSync(legacySkills, { withFileTypes: true });
|
|
396
|
+
} catch (error) {
|
|
397
|
+
const code = error && error.code ? ` (${error.code})` : '';
|
|
398
|
+
process.stderr.write(`Warning: failed to list deprecated .claude/skills/${code}; left untouched. Remove it manually.\n`);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
402
|
+
const backupRoot = options.backupDir
|
|
403
|
+
? path.resolve(cwd, options.backupDir)
|
|
404
|
+
: path.join(cwd, '.code-flow', 'backups', stamp);
|
|
405
|
+
for (const entry of entries) {
|
|
406
|
+
if (!MANAGED_LEGACY_SKILL_NAMES.has(entry.name)) continue; // user-owned: keep
|
|
407
|
+
const src = path.join(legacySkills, entry.name);
|
|
408
|
+
const rel = path.join('.claude', 'skills', entry.name);
|
|
409
|
+
try {
|
|
410
|
+
const dest = path.join(backupRoot, rel);
|
|
411
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
412
|
+
if (entry.isDirectory()) {
|
|
413
|
+
copyDirRecursive(src, dest, true);
|
|
414
|
+
} else if (entry.isFile()) {
|
|
415
|
+
fs.copyFileSync(src, dest);
|
|
416
|
+
} else {
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
fs.rmSync(src, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
|
|
420
|
+
removed.push(`${rel} (backup: ${path.relative(cwd, dest)})`);
|
|
421
|
+
} catch (error) {
|
|
422
|
+
const code = error && error.code ? ` (${error.code})` : '';
|
|
423
|
+
const message = error && error.message ? error.message : String(error);
|
|
424
|
+
process.stderr.write(`Warning: failed to migrate deprecated ${rel}${code}: ${message}\n`);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
345
429
|
// Files renamed in 0.x → 1.x that may linger in upgraded projects.
|
|
346
430
|
// Each entry: { path: relative-to-cwd, replacedBy: hint string for the warning }.
|
|
347
431
|
const ORPHAN_FILES = [
|
|
@@ -367,35 +451,30 @@ function removeOrphanFiles(cwd, removed) {
|
|
|
367
451
|
}
|
|
368
452
|
}
|
|
369
453
|
|
|
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
|
-
}
|
|
454
|
+
// Skill entries historically deployed by code-flow 0.x under .claude/skills/.
|
|
455
|
+
// Only these names are managed legacy content; any other entry is user-owned.
|
|
456
|
+
const MANAGED_LEGACY_SKILL_NAMES = new Set([
|
|
457
|
+
'cf-init.md',
|
|
458
|
+
'cf-inject.md',
|
|
459
|
+
'cf-learn.md',
|
|
460
|
+
'cf-scan.md',
|
|
461
|
+
'cf-stats.md',
|
|
462
|
+
'cf-validate.md',
|
|
463
|
+
]);
|
|
394
464
|
|
|
395
465
|
// --- pyyaml dependency: probe first, fall back through PEP 668 strategies ---
|
|
396
466
|
|
|
397
|
-
|
|
398
|
-
|
|
467
|
+
// --- pyyaml dependency: probe first, fall back through PEP 668 strategies ---
|
|
468
|
+
// Bounded waits: each attempt carries its own timeout inside a total budget,
|
|
469
|
+
// with progress on stderr. spawnFn/logFn/nowFn are injectable for tests.
|
|
470
|
+
const PIP_PER_ATTEMPT_MS = 60000;
|
|
471
|
+
const PIP_TOTAL_MS = 120000;
|
|
472
|
+
|
|
473
|
+
function ensurePyYaml(spawnFn, logFn, nowFn) {
|
|
474
|
+
const spawn = spawnFn || spawnSync;
|
|
475
|
+
const log = logFn || ((message) => process.stderr.write(message));
|
|
476
|
+
const now = nowFn || Date.now;
|
|
477
|
+
const probe = spawn('python3', ['-c', 'import yaml'], { stdio: 'ignore' });
|
|
399
478
|
if (!probe.error && probe.status === 0) return; // already installed
|
|
400
479
|
|
|
401
480
|
const attempts = [
|
|
@@ -403,11 +482,18 @@ function ensurePyYaml() {
|
|
|
403
482
|
['-m', 'pip', 'install', 'pyyaml'],
|
|
404
483
|
['-m', 'pip', 'install', '--user', '--break-system-packages', 'pyyaml'],
|
|
405
484
|
];
|
|
406
|
-
|
|
407
|
-
|
|
485
|
+
const started = now();
|
|
486
|
+
for (let i = 0; i < attempts.length; i++) {
|
|
487
|
+
const remaining = PIP_TOTAL_MS - (now() - started);
|
|
488
|
+
if (remaining <= 0) break;
|
|
489
|
+
log(`Installing pyyaml (attempt ${i + 1}/${attempts.length})...\n`);
|
|
490
|
+
const result = spawn('python3', attempts[i], {
|
|
491
|
+
stdio: 'ignore',
|
|
492
|
+
timeout: Math.min(PIP_PER_ATTEMPT_MS, remaining),
|
|
493
|
+
});
|
|
408
494
|
if (!result.error && result.status === 0) return;
|
|
409
495
|
}
|
|
410
|
-
|
|
496
|
+
log(
|
|
411
497
|
'Warning: pyyaml install failed. Run manually:\n' +
|
|
412
498
|
' python3 -m pip install --user pyyaml\n' +
|
|
413
499
|
' # or, on PEP 668 systems:\n' +
|
|
@@ -468,10 +554,21 @@ function parsePlatform(args) {
|
|
|
468
554
|
return null;
|
|
469
555
|
}
|
|
470
556
|
|
|
557
|
+
function parseStringFlag(args, name) {
|
|
558
|
+
const prefix = `--${name}=`;
|
|
559
|
+
for (const arg of args) {
|
|
560
|
+
if (arg.startsWith(prefix)) return arg.slice(prefix.length);
|
|
561
|
+
}
|
|
562
|
+
const idx = args.indexOf(`--${name}`);
|
|
563
|
+
if (idx !== -1) return args[idx + 1] || '';
|
|
564
|
+
return '';
|
|
565
|
+
}
|
|
566
|
+
|
|
471
567
|
// --- Main init ---
|
|
472
568
|
|
|
473
|
-
function runInit(force, platform) {
|
|
569
|
+
function runInit(force, platform, opts) {
|
|
474
570
|
const cwd = process.cwd();
|
|
571
|
+
const options = opts || {};
|
|
475
572
|
const installedVersion = readInstalledVersion(cwd);
|
|
476
573
|
if (installedVersion && compareVersions(installedVersion, '0.4.2') >= 0 && compareVersions(installedVersion, '0.6.0') < 0) {
|
|
477
574
|
process.stdout.write(`${JSON.stringify({ status: 'migration_required', version: installedVersion, command: 'code-flow migrate --spec-workflow --dry-run' })}\n`);
|
|
@@ -483,16 +580,28 @@ function runInit(force, platform) {
|
|
|
483
580
|
const coreDir = path.join(baseDir, 'core');
|
|
484
581
|
const adaptersDir = path.join(baseDir, 'adapters');
|
|
485
582
|
|
|
486
|
-
// Determine mode
|
|
487
|
-
|
|
583
|
+
// Determine mode: global mode drives core files and messaging; each
|
|
584
|
+
// platform additionally resolves its own mode from per-platform records so
|
|
585
|
+
// upgrading one platform can never mask another platform's staleness.
|
|
586
|
+
// A global upgrade also refreshes the selected platform (previous behavior).
|
|
587
|
+
const mode = modeFor(installedVersion, force);
|
|
588
|
+
const adapterVersions = readAdapterVersions(cwd);
|
|
589
|
+
const adapterPrev = typeof adapterVersions.adapters[platform] === 'string'
|
|
590
|
+
? adapterVersions.adapters[platform]
|
|
591
|
+
: null;
|
|
592
|
+
let platformMode;
|
|
488
593
|
if (force) {
|
|
489
|
-
|
|
490
|
-
} else if (!
|
|
491
|
-
|
|
492
|
-
} else if (
|
|
493
|
-
|
|
594
|
+
platformMode = 'force';
|
|
595
|
+
} else if (!adapterPrev && !platformFilesExist(cwd, platform)) {
|
|
596
|
+
platformMode = 'fresh';
|
|
597
|
+
} else if (
|
|
598
|
+
(adapterPrev && compareVersions(adapterPrev, pkg.version) < 0) ||
|
|
599
|
+
(!adapterPrev && platformFilesExist(cwd, platform)) ||
|
|
600
|
+
mode === 'upgrade'
|
|
601
|
+
) {
|
|
602
|
+
platformMode = 'upgrade';
|
|
494
603
|
} else {
|
|
495
|
-
|
|
604
|
+
platformMode = 'current';
|
|
496
605
|
}
|
|
497
606
|
|
|
498
607
|
const created = [];
|
|
@@ -502,8 +611,10 @@ function runInit(force, platform) {
|
|
|
502
611
|
const removed = [];
|
|
503
612
|
const results = { created, updated, merged, skipped };
|
|
504
613
|
|
|
505
|
-
// Track + copy helper for directory trees
|
|
506
|
-
|
|
614
|
+
// Track + copy helper for directory trees. dirMode is resolved per area:
|
|
615
|
+
// core files follow the global mode, platform files follow platformMode.
|
|
616
|
+
const processDir = (srcDir, destDir, prefix, dirMode) => {
|
|
617
|
+
const effective = dirMode || mode;
|
|
507
618
|
if (!fs.existsSync(srcDir)) return;
|
|
508
619
|
const files = collectFiles(srcDir, '');
|
|
509
620
|
for (const rel of files) {
|
|
@@ -516,10 +627,10 @@ function runInit(force, platform) {
|
|
|
516
627
|
created.push(label);
|
|
517
628
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
518
629
|
fs.copyFileSync(src, dest);
|
|
519
|
-
} else if (
|
|
630
|
+
} else if (effective === 'force') {
|
|
520
631
|
updated.push(label);
|
|
521
632
|
fs.copyFileSync(src, dest);
|
|
522
|
-
} else if (
|
|
633
|
+
} else if (effective === 'upgrade' && cat === 'tool') {
|
|
523
634
|
updated.push(label);
|
|
524
635
|
fs.copyFileSync(src, dest);
|
|
525
636
|
} else {
|
|
@@ -529,7 +640,7 @@ function runInit(force, platform) {
|
|
|
529
640
|
};
|
|
530
641
|
|
|
531
642
|
// Process .code-flow/ (core)
|
|
532
|
-
processDir(path.join(coreDir, 'code-flow'), path.join(cwd, '.code-flow'), '.code-flow');
|
|
643
|
+
processDir(path.join(coreDir, 'code-flow'), path.join(cwd, '.code-flow'), '.code-flow', mode);
|
|
533
644
|
|
|
534
645
|
// Process Claude adapter
|
|
535
646
|
if (platform === 'claude') {
|
|
@@ -537,7 +648,7 @@ function runInit(force, platform) {
|
|
|
537
648
|
src: path.join(adaptersDir, 'claude', 'CLAUDE.md'),
|
|
538
649
|
dest: path.join(cwd, 'CLAUDE.md'),
|
|
539
650
|
label: 'CLAUDE.md',
|
|
540
|
-
mode,
|
|
651
|
+
mode: platformMode,
|
|
541
652
|
mergeFn: mergeClaudeMd,
|
|
542
653
|
toolOnUpgrade: false,
|
|
543
654
|
results,
|
|
@@ -547,14 +658,14 @@ function runInit(force, platform) {
|
|
|
547
658
|
processDir(
|
|
548
659
|
path.join(adaptersDir, 'claude', 'commands'),
|
|
549
660
|
path.join(cwd, '.claude', 'commands'),
|
|
550
|
-
'.claude/commands'
|
|
661
|
+
'.claude/commands', platformMode
|
|
551
662
|
);
|
|
552
663
|
|
|
553
664
|
installAdapterFile({
|
|
554
665
|
src: path.join(adaptersDir, 'claude', 'settings.local.json'),
|
|
555
666
|
dest: path.join(cwd, '.claude', 'settings.local.json'),
|
|
556
667
|
label: '.claude/settings.local.json',
|
|
557
|
-
mode,
|
|
668
|
+
mode: platformMode,
|
|
558
669
|
mergeFn: mergeSettingsJson,
|
|
559
670
|
toolOnUpgrade: false,
|
|
560
671
|
results,
|
|
@@ -567,7 +678,7 @@ function runInit(force, platform) {
|
|
|
567
678
|
src: path.join(adaptersDir, 'costrict', 'CLAUDE.md'),
|
|
568
679
|
dest: path.join(cwd, 'CLAUDE.md'),
|
|
569
680
|
label: 'CLAUDE.md',
|
|
570
|
-
mode,
|
|
681
|
+
mode: platformMode,
|
|
571
682
|
mergeFn: mergeClaudeMd,
|
|
572
683
|
toolOnUpgrade: false,
|
|
573
684
|
results,
|
|
@@ -577,14 +688,14 @@ function runInit(force, platform) {
|
|
|
577
688
|
processDir(
|
|
578
689
|
path.join(adaptersDir, 'costrict', 'commands'),
|
|
579
690
|
path.join(cwd, '.costrict', 'commands'),
|
|
580
|
-
'.costrict/commands'
|
|
691
|
+
'.costrict/commands', platformMode
|
|
581
692
|
);
|
|
582
693
|
|
|
583
694
|
installAdapterFile({
|
|
584
695
|
src: path.join(adaptersDir, 'costrict', 'settings.local.json'),
|
|
585
696
|
dest: path.join(cwd, '.costrict', 'settings.local.json'),
|
|
586
697
|
label: '.costrict/settings.local.json',
|
|
587
|
-
mode,
|
|
698
|
+
mode: platformMode,
|
|
588
699
|
mergeFn: mergeSettingsJson,
|
|
589
700
|
toolOnUpgrade: false,
|
|
590
701
|
results,
|
|
@@ -597,7 +708,7 @@ function runInit(force, platform) {
|
|
|
597
708
|
src: path.join(adaptersDir, 'codex', 'AGENTS.md'),
|
|
598
709
|
dest: path.join(cwd, 'AGENTS.md'),
|
|
599
710
|
label: 'AGENTS.md',
|
|
600
|
-
mode,
|
|
711
|
+
mode: platformMode,
|
|
601
712
|
mergeFn: mergeClaudeMd,
|
|
602
713
|
toolOnUpgrade: false,
|
|
603
714
|
results,
|
|
@@ -607,7 +718,7 @@ function runInit(force, platform) {
|
|
|
607
718
|
src: path.join(adaptersDir, 'codex', 'hooks.json'),
|
|
608
719
|
dest: path.join(cwd, '.codex', 'hooks.json'),
|
|
609
720
|
label: '.codex/hooks.json',
|
|
610
|
-
mode,
|
|
721
|
+
mode: platformMode,
|
|
611
722
|
mergeFn: mergeSettingsJson,
|
|
612
723
|
toolOnUpgrade: false,
|
|
613
724
|
results,
|
|
@@ -617,7 +728,7 @@ function runInit(force, platform) {
|
|
|
617
728
|
src: path.join(adaptersDir, 'codex', 'config.toml'),
|
|
618
729
|
dest: path.join(cwd, '.codex', 'config.toml'),
|
|
619
730
|
label: '.codex/config.toml',
|
|
620
|
-
mode,
|
|
731
|
+
mode: platformMode,
|
|
621
732
|
mergeFn: mergeCodexConfigToml,
|
|
622
733
|
toolOnUpgrade: false,
|
|
623
734
|
results,
|
|
@@ -627,7 +738,7 @@ function runInit(force, platform) {
|
|
|
627
738
|
processDir(
|
|
628
739
|
path.join(adaptersDir, 'codex', 'skills'),
|
|
629
740
|
path.join(cwd, '.agents', 'skills'),
|
|
630
|
-
'.agents/skills'
|
|
741
|
+
'.agents/skills', platformMode
|
|
631
742
|
);
|
|
632
743
|
}
|
|
633
744
|
|
|
@@ -637,7 +748,7 @@ function runInit(force, platform) {
|
|
|
637
748
|
src: path.join(adaptersDir, 'opencode', 'AGENTS.md'),
|
|
638
749
|
dest: path.join(cwd, 'AGENTS.md'),
|
|
639
750
|
label: 'AGENTS.md',
|
|
640
|
-
mode,
|
|
751
|
+
mode: platformMode,
|
|
641
752
|
mergeFn: mergeClaudeMd,
|
|
642
753
|
toolOnUpgrade: false,
|
|
643
754
|
results,
|
|
@@ -647,7 +758,7 @@ function runInit(force, platform) {
|
|
|
647
758
|
processDir(
|
|
648
759
|
path.join(adaptersDir, 'opencode', 'plugins'),
|
|
649
760
|
path.join(cwd, '.opencode', 'plugins'),
|
|
650
|
-
'.opencode/plugins'
|
|
761
|
+
'.opencode/plugins', platformMode
|
|
651
762
|
);
|
|
652
763
|
|
|
653
764
|
// Stamp main package version into the plugin's package.json
|
|
@@ -662,14 +773,14 @@ function runInit(force, platform) {
|
|
|
662
773
|
processDir(
|
|
663
774
|
path.join(adaptersDir, 'opencode', 'commands'),
|
|
664
775
|
path.join(cwd, '.opencode', 'commands'),
|
|
665
|
-
'.opencode/commands'
|
|
776
|
+
'.opencode/commands', platformMode
|
|
666
777
|
);
|
|
667
778
|
|
|
668
779
|
installAdapterFile({
|
|
669
780
|
src: path.join(adaptersDir, 'opencode', 'opencode.json'),
|
|
670
781
|
dest: path.join(cwd, 'opencode.json'),
|
|
671
782
|
label: 'opencode.json',
|
|
672
|
-
mode,
|
|
783
|
+
mode: platformMode,
|
|
673
784
|
mergeFn: mergeSettingsJson,
|
|
674
785
|
toolOnUpgrade: false,
|
|
675
786
|
results,
|
|
@@ -689,8 +800,8 @@ function runInit(force, platform) {
|
|
|
689
800
|
}
|
|
690
801
|
}
|
|
691
802
|
|
|
692
|
-
// Clean up legacy .claude/skills/
|
|
693
|
-
removeLegacyClaudeSkills(cwd, removed);
|
|
803
|
+
// Clean up legacy .claude/skills/ (claude domain only; user-owned kept, managed backed up)
|
|
804
|
+
removeLegacyClaudeSkills(cwd, removed, { platform, backupDir: options.backupDir || '' });
|
|
694
805
|
|
|
695
806
|
// Clean up renamed orphan scripts (only meaningful on upgrade)
|
|
696
807
|
if (mode === 'upgrade' || mode === 'force') {
|
|
@@ -700,13 +811,16 @@ function runInit(force, platform) {
|
|
|
700
811
|
// Install pyyaml — probe first, fall back through PEP 668 strategies
|
|
701
812
|
ensurePyYaml();
|
|
702
813
|
|
|
703
|
-
// Write version
|
|
814
|
+
// Write version (legacy global file kept for compatibility) + per-platform records
|
|
704
815
|
writeVersion(cwd, pkg.version);
|
|
816
|
+
adapterVersions.adapters[platform] = pkg.version;
|
|
817
|
+
adapterVersions.core = pkg.version;
|
|
818
|
+
writeAdapterVersions(cwd, adapterVersions);
|
|
705
819
|
|
|
706
820
|
// Output summary
|
|
707
821
|
if (mode === 'upgrade') {
|
|
708
822
|
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) {
|
|
823
|
+
} else if (mode === 'current' && platformMode === 'current' && created.length === 0 && updated.length === 0 && merged.length === 0) {
|
|
710
824
|
process.stdout.write(`code-flow v${pkg.version} already up to date.\n`);
|
|
711
825
|
process.exit(0);
|
|
712
826
|
} else if (mode === 'force') {
|
|
@@ -841,7 +955,7 @@ if (args[0] === 'init') {
|
|
|
841
955
|
if (platform !== 'claude' && platform !== 'codex' && platform !== 'costrict' && platform !== 'opencode') {
|
|
842
956
|
fail(`Error: --platform must be "claude", "codex", "costrict", or "opencode", got "${platform}".`);
|
|
843
957
|
}
|
|
844
|
-
runInit(force, platform);
|
|
958
|
+
runInit(force, platform, { backupDir: parseStringFlag(args, 'backup-dir') });
|
|
845
959
|
}
|
|
846
960
|
|
|
847
961
|
if (args[0] === 'migrate') {
|
|
@@ -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)
|