@michaelxwb/code-flow 0.6.5 → 1.0.0
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 +75 -110
- package/bin/code-flow.js +12 -0
- package/docs/README.md +19 -0
- package/docs/USAGE.md +268 -0
- package/docs/v1-design.md +1858 -0
- package/docs/v1-implementation.md +186 -0
- package/lib/adapters.js +126 -0
- package/lib/cli.js +258 -0
- package/lib/config.js +85 -0
- package/lib/doctor.js +283 -0
- package/lib/frontmatter.js +42 -0
- package/lib/fs.js +141 -0
- package/lib/glob.js +23 -0
- package/lib/hook.js +99 -0
- package/lib/init.js +132 -0
- package/lib/learn.js +170 -0
- package/lib/locks.js +52 -0
- package/lib/markdown.js +13 -0
- package/lib/process.js +116 -0
- package/lib/render.js +115 -0
- package/lib/repo.js +48 -0
- package/lib/runs.js +155 -0
- package/lib/session.js +40 -0
- package/lib/spec.js +80 -0
- package/lib/task.js +258 -0
- package/lib/taskdoc.js +308 -0
- package/lib/toml.js +140 -0
- package/lib/verify.js +168 -0
- package/lib/worktree.js +930 -0
- package/package.json +21 -9
- package/templates/adapters/opencode.mjs +65 -0
- package/templates/config.json +39 -0
- package/templates/instructions.md +13 -0
- package/templates/launcher/bridge.cjs +295 -0
- package/templates/launcher/hook.cmd +15 -0
- package/templates/launcher/hook.ps1 +10 -0
- package/templates/launcher/hook.sh +20 -0
- package/templates/skills/cf-check.md +19 -0
- package/templates/skills/cf-init.md +16 -0
- package/templates/skills/cf-spec.md +20 -0
- package/templates/skills/cf-task-align.md +19 -0
- package/templates/skills/cf-task-archive.md +20 -0
- package/templates/skills/cf-task-plan.md +31 -0
- package/templates/skills/cf-task-prd.md +14 -0
- package/templates/skills/cf-task-start.md +41 -0
- package/{src/core/code-flow/specs/shared/design → templates/workflow}/design-frontend.md +22 -19
- package/{src/core/code-flow/specs/shared/design → templates/workflow}/design-full.md +36 -28
- package/{src/core/code-flow/specs/shared/design → templates/workflow}/design-lite.md +27 -17
- package/{src/core/code-flow/specs/shared/prd-template.md → templates/workflow/prd.md} +1 -1
- package/templates/workflow/project-guidelines.md +48 -0
- package/templates/workflow/scenarios.md +67 -0
- package/templates/workflow/task.md +56 -0
- package/docs/migrations/spec-workflow-0.6.0.md +0 -42
- package/src/adapters/claude/CLAUDE.md +0 -45
- package/src/adapters/claude/commands/cf-init.md +0 -264
- package/src/adapters/claude/commands/cf-learn.md +0 -291
- package/src/adapters/claude/commands/cf-spec.md +0 -67
- package/src/adapters/claude/commands/cf-stats.md +0 -86
- package/src/adapters/claude/commands/cf-sync.md +0 -25
- package/src/adapters/claude/commands/cf-task/align.md +0 -309
- package/src/adapters/claude/commands/cf-task/archive.md +0 -161
- package/src/adapters/claude/commands/cf-task/block.md +0 -36
- package/src/adapters/claude/commands/cf-task/graph.md +0 -80
- package/src/adapters/claude/commands/cf-task/note.md +0 -236
- package/src/adapters/claude/commands/cf-task/plan.md +0 -363
- package/src/adapters/claude/commands/cf-task/prd.md +0 -162
- package/src/adapters/claude/commands/cf-task/start.md +0 -304
- package/src/adapters/claude/commands/cf-task/status.md +0 -59
- package/src/adapters/claude/commands/cf-task/verify-e2e.md +0 -115
- package/src/adapters/claude/commands/cf-validate.md +0 -75
- package/src/adapters/claude/settings.local.json +0 -50
- package/src/adapters/codex/AGENTS.md +0 -45
- package/src/adapters/codex/config.toml +0 -3
- package/src/adapters/codex/hooks.json +0 -50
- package/src/adapters/codex/skills/cf-init/SKILL.md +0 -273
- package/src/adapters/codex/skills/cf-learn/SKILL.md +0 -296
- package/src/adapters/codex/skills/cf-spec/SKILL.md +0 -60
- package/src/adapters/codex/skills/cf-stats/SKILL.md +0 -87
- package/src/adapters/codex/skills/cf-sync/SKILL.md +0 -30
- package/src/adapters/codex/skills/cf-task-align/SKILL.md +0 -310
- package/src/adapters/codex/skills/cf-task-archive/SKILL.md +0 -162
- package/src/adapters/codex/skills/cf-task-block/SKILL.md +0 -37
- package/src/adapters/codex/skills/cf-task-graph/SKILL.md +0 -81
- package/src/adapters/codex/skills/cf-task-note/SKILL.md +0 -233
- package/src/adapters/codex/skills/cf-task-plan/SKILL.md +0 -324
- package/src/adapters/codex/skills/cf-task-prd/SKILL.md +0 -163
- package/src/adapters/codex/skills/cf-task-start/SKILL.md +0 -305
- package/src/adapters/codex/skills/cf-task-status/SKILL.md +0 -60
- package/src/adapters/codex/skills/cf-task-verify-e2e/SKILL.md +0 -111
- package/src/adapters/codex/skills/cf-validate/SKILL.md +0 -76
- package/src/adapters/costrict/CLAUDE.md +0 -45
- package/src/adapters/costrict/commands/cf-init.md +0 -264
- package/src/adapters/costrict/commands/cf-learn.md +0 -291
- package/src/adapters/costrict/commands/cf-spec.md +0 -67
- package/src/adapters/costrict/commands/cf-stats.md +0 -86
- package/src/adapters/costrict/commands/cf-sync.md +0 -25
- package/src/adapters/costrict/commands/cf-task/align.md +0 -309
- package/src/adapters/costrict/commands/cf-task/archive.md +0 -161
- package/src/adapters/costrict/commands/cf-task/block.md +0 -36
- package/src/adapters/costrict/commands/cf-task/graph.md +0 -80
- package/src/adapters/costrict/commands/cf-task/note.md +0 -236
- package/src/adapters/costrict/commands/cf-task/plan.md +0 -363
- package/src/adapters/costrict/commands/cf-task/prd.md +0 -162
- package/src/adapters/costrict/commands/cf-task/start.md +0 -304
- package/src/adapters/costrict/commands/cf-task/status.md +0 -59
- package/src/adapters/costrict/commands/cf-task/verify-e2e.md +0 -115
- package/src/adapters/costrict/commands/cf-validate.md +0 -75
- package/src/adapters/costrict/settings.local.json +0 -50
- package/src/adapters/opencode/AGENTS.md +0 -45
- package/src/adapters/opencode/commands/cf-init.md +0 -281
- package/src/adapters/opencode/commands/cf-learn.md +0 -295
- package/src/adapters/opencode/commands/cf-spec.md +0 -59
- package/src/adapters/opencode/commands/cf-stats.md +0 -90
- package/src/adapters/opencode/commands/cf-sync.md +0 -29
- package/src/adapters/opencode/commands/cf-task/align.md +0 -314
- package/src/adapters/opencode/commands/cf-task/archive.md +0 -166
- package/src/adapters/opencode/commands/cf-task/block.md +0 -40
- package/src/adapters/opencode/commands/cf-task/graph.md +0 -84
- package/src/adapters/opencode/commands/cf-task/note.md +0 -240
- package/src/adapters/opencode/commands/cf-task/plan.md +0 -363
- package/src/adapters/opencode/commands/cf-task/prd.md +0 -167
- package/src/adapters/opencode/commands/cf-task/start.md +0 -309
- package/src/adapters/opencode/commands/cf-task/status.md +0 -63
- package/src/adapters/opencode/commands/cf-task/verify-e2e.md +0 -115
- package/src/adapters/opencode/commands/cf-validate.md +0 -79
- package/src/adapters/opencode/opencode.json +0 -3
- package/src/adapters/opencode/plugins/code-flow/index.js +0 -213
- package/src/adapters/opencode/plugins/code-flow/package.json +0 -6
- package/src/cli.js +0 -1023
- package/src/core/code-flow/config.yml +0 -158
- package/src/core/code-flow/scripts/cf_acceptance_evidence.py +0 -100
- package/src/core/code-flow/scripts/cf_acceptance_manifest.py +0 -294
- package/src/core/code-flow/scripts/cf_acceptance_runner.py +0 -192
- package/src/core/code-flow/scripts/cf_acceptance_schema.py +0 -83
- package/src/core/code-flow/scripts/cf_checks.py +0 -336
- package/src/core/code-flow/scripts/cf_core.py +0 -787
- package/src/core/code-flow/scripts/cf_exec_base.py +0 -112
- package/src/core/code-flow/scripts/cf_feedback.py +0 -56
- package/src/core/code-flow/scripts/cf_log.py +0 -154
- package/src/core/code-flow/scripts/cf_post_hook.py +0 -196
- package/src/core/code-flow/scripts/cf_pre_tool_hook.py +0 -104
- package/src/core/code-flow/scripts/cf_scan.py +0 -295
- package/src/core/code-flow/scripts/cf_session_state.py +0 -37
- package/src/core/code-flow/scripts/cf_spec_context.py +0 -1746
- package/src/core/code-flow/scripts/cf_spec_gate.py +0 -264
- package/src/core/code-flow/scripts/cf_spec_metadata.py +0 -384
- package/src/core/code-flow/scripts/cf_spec_migrate.py +0 -512
- package/src/core/code-flow/scripts/cf_spec_resolver.py +0 -347
- package/src/core/code-flow/scripts/cf_spec_router.py +0 -244
- package/src/core/code-flow/scripts/cf_spec_session.py +0 -119
- package/src/core/code-flow/scripts/cf_spec_verify.py +0 -559
- package/src/core/code-flow/scripts/cf_stats.py +0 -506
- package/src/core/code-flow/scripts/cf_stop_hook.py +0 -381
- package/src/core/code-flow/scripts/cf_sync.py +0 -140
- package/src/core/code-flow/scripts/cf_task_index.py +0 -148
- package/src/core/code-flow/scripts/cf_task_parallel.py +0 -394
- package/src/core/code-flow/scripts/cf_task_runtime.py +0 -304
- package/src/core/code-flow/scripts/cf_task_state.py +0 -3
- package/src/core/code-flow/scripts/cf_task_workflow.py +0 -205
- package/src/core/code-flow/scripts/cf_user_prompt_hook.py +0 -178
- package/src/core/code-flow/scripts/cf_validation.py +0 -81
- package/src/core/code-flow/scripts/cf_workflow_service.py +0 -366
- package/src/core/code-flow/scripts/cf_workflow_transaction.py +0 -96
- package/src/core/code-flow/specs/backend/_map.md +0 -56
- package/src/core/code-flow/specs/backend/code-quality-performance.md +0 -59
- package/src/core/code-flow/specs/backend/database.md +0 -51
- package/src/core/code-flow/specs/backend/directory-structure.md +0 -54
- package/src/core/code-flow/specs/backend/logging.md +0 -50
- package/src/core/code-flow/specs/backend/platform-rules.md +0 -55
- package/src/core/code-flow/specs/frontend/_map.md +0 -52
- package/src/core/code-flow/specs/frontend/component-specs.md +0 -68
- package/src/core/code-flow/specs/frontend/directory-structure.md +0 -53
- package/src/core/code-flow/specs/frontend/quality-standards.md +0 -58
- package/src/core/code-flow/specs/shared/_map.md +0 -57
- package/src/core/code-flow/tasks/.gitkeep +0 -0
- package/src/core/code-flow/validation.yml +0 -53
- package/src/migrate/spec-workflow.js +0 -238
package/lib/hook.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
const f = require('./fs');
|
|
4
|
+
const config = require('./config');
|
|
5
|
+
const session = require('./session');
|
|
6
|
+
const task = require('./task');
|
|
7
|
+
const render = require('./render');
|
|
8
|
+
const markdown = require('./markdown');
|
|
9
|
+
function cleanPrompt(text) {
|
|
10
|
+
let fence = null;
|
|
11
|
+
return text.replace(/<(task-notification|output-file)\b[^>]*>[\s\S]*?(?:<\/\1>|$)/gi, '')
|
|
12
|
+
.split('\n').filter(line => {
|
|
13
|
+
const marker = markdown.fenceMarker(line, fence);
|
|
14
|
+
if (marker) {
|
|
15
|
+
fence = marker.next;
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
return !fence && !/^\s*>/.test(line);
|
|
19
|
+
}).join('\n').replace(/\b[a-z]+:\/\/[^\s<>]+/gi, '');
|
|
20
|
+
}
|
|
21
|
+
function paths(prompt, root) {
|
|
22
|
+
const text = prompt.replace(/^task:[^\n]*$/gm, '');
|
|
23
|
+
const found = [];
|
|
24
|
+
const pattern = /(?:@)?(?:"([^"\r\n]+)"|'([^'\r\n]+)'|`([^`\r\n]+)`|([A-Za-z0-9_.@/\\:-]+))/g;
|
|
25
|
+
for (const match of text.matchAll(pattern)) {
|
|
26
|
+
const value = (match[1] || match[2] || match[3] || match[4]).replace(/^@/, '').replace(/[.,;:]+$/, '');
|
|
27
|
+
if (!/[\\/]/.test(value) && !/\.[A-Za-z][A-Za-z0-9]{0,9}$/.test(value)) continue;
|
|
28
|
+
try { found.push(...render.normalizePaths(root, [value])); } catch (error) {
|
|
29
|
+
if (error.code !== 'invalid_path') throw error;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return [...new Set(found)];
|
|
33
|
+
}
|
|
34
|
+
function nativeEvent(data, format) {
|
|
35
|
+
f.object(data, 'Hook input');
|
|
36
|
+
if (format === 'opencode') {
|
|
37
|
+
f.object(data.event, 'OpenCode event');
|
|
38
|
+
return { cwd: data.directory, sessionId: data.event.sessionID, prompt: data.event.prompt?.text };
|
|
39
|
+
}
|
|
40
|
+
f.demand(['claude', 'codex', 'costrict'].includes(format), 'hook_format', 'Unknown Hook format');
|
|
41
|
+
return { cwd: data.cwd, sessionId: data.session_id, prompt: data.prompt };
|
|
42
|
+
}
|
|
43
|
+
function parseTaskRef(value) {
|
|
44
|
+
const match = /^(.+\.md)#(TASK-\d+)$/.exec(String(value).trim());
|
|
45
|
+
f.demand(match, 'task_reference', 'Use task: <task-document>.md#TASK-NNN');
|
|
46
|
+
return { taskFile: match[1], taskId: match[2] };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function choose(root, prompt, token, diagnostics) {
|
|
50
|
+
const explicit = [...prompt.matchAll(/^task:[ \t]*(.+?)[ \t]*$/gm)].map(match => match[1]);
|
|
51
|
+
if (explicit.length) {
|
|
52
|
+
if (explicit.length !== 1) diagnostics.push({ code: 'task_ambiguous', message: 'Use exactly one task: <task-document>.md#TASK-NNN' });
|
|
53
|
+
else {
|
|
54
|
+
try {
|
|
55
|
+
const ref = parseTaskRef(explicit[0]);
|
|
56
|
+
const selected = task.loadTask(root, ref.taskFile, ref.taskId);
|
|
57
|
+
return { taskFile: selected.relative, taskId: selected.id };
|
|
58
|
+
} catch (error) { diagnostics.push({ code: error.code, message: error.message }); }
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
if (!token) return null;
|
|
63
|
+
try {
|
|
64
|
+
const binding = session.read(root, token);
|
|
65
|
+
if (!binding) return null;
|
|
66
|
+
const selected = task.loadTask(root, binding.task_file, binding.task_id);
|
|
67
|
+
f.demand(selected.status === 'in-progress', 'binding_stale', 'Bound Task is no longer in-progress');
|
|
68
|
+
return { taskFile: selected.relative, taskId: selected.id };
|
|
69
|
+
} catch (error) { diagnostics.push({ code: error.code || 'binding_invalid', message: error.message }); return null; }
|
|
70
|
+
}
|
|
71
|
+
function payload(format, text) {
|
|
72
|
+
return format === 'opencode' ? { context: text } :
|
|
73
|
+
{ hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext: text } };
|
|
74
|
+
}
|
|
75
|
+
function build(data, format) {
|
|
76
|
+
const diagnostics = [];
|
|
77
|
+
try {
|
|
78
|
+
const event = nativeEvent(data, format);
|
|
79
|
+
f.demand(typeof event.prompt === 'string', 'hook_prompt', 'prompt must be a string');
|
|
80
|
+
if (!event.prompt.trim()) return { payload: null, diagnostics };
|
|
81
|
+
f.string(event.cwd, 'Hook cwd');
|
|
82
|
+
f.demand(path.isAbsolute(event.cwd), 'hook_cwd', 'Hook cwd must be absolute');
|
|
83
|
+
const root = config.findRoot(event.cwd, false);
|
|
84
|
+
if (!root) return { payload: null, diagnostics };
|
|
85
|
+
config.load(root);
|
|
86
|
+
const prompt = cleanPrompt(event.prompt);
|
|
87
|
+
const token = typeof event.sessionId === 'string' && event.sessionId ?
|
|
88
|
+
session.token(root, format, event.sessionId) : null;
|
|
89
|
+
if (!token) diagnostics.push({ code: 'session_missing', message: 'No session ID; explicit paths remain available' });
|
|
90
|
+
const selected = choose(root, prompt, token, diagnostics);
|
|
91
|
+
const result = render.inject(root, { taskFile: selected?.taskFile, taskId: selected?.taskId,
|
|
92
|
+
paths: paths(prompt, root), session: token });
|
|
93
|
+
return { ...result, payload: payload(format, result.text), diagnostics: [...diagnostics, ...result.diagnostics] };
|
|
94
|
+
} catch (error) {
|
|
95
|
+
diagnostics.push({ code: error.code || 'hook_error', message: error.message });
|
|
96
|
+
return { payload: payload(format, ''), diagnostics };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
module.exports = { build, payload, paths, cleanPrompt, choose, parseTaskRef, nativeEvent };
|
package/lib/init.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const f = require('./fs');
|
|
5
|
+
const config = require('./config');
|
|
6
|
+
const adapters = require('./adapters');
|
|
7
|
+
const templateRoot = path.join(__dirname, '../templates');
|
|
8
|
+
const SKILLS = [
|
|
9
|
+
{ name: 'cf-init', path: 'cf-init' },
|
|
10
|
+
{ name: 'cf-check', path: 'cf-check' },
|
|
11
|
+
{ name: 'cf-spec', path: 'cf-spec' },
|
|
12
|
+
{ name: 'cf-task-prd', path: 'cf-task/prd' },
|
|
13
|
+
{ name: 'cf-task-align', path: 'cf-task/align' },
|
|
14
|
+
{ name: 'cf-task-plan', path: 'cf-task/plan' },
|
|
15
|
+
{ name: 'cf-task-start', path: 'cf-task/start' },
|
|
16
|
+
{ name: 'cf-task-archive', path: 'cf-task/archive' }
|
|
17
|
+
];
|
|
18
|
+
const WORKFLOW_TEMPLATES = ['prd.md', 'design-lite.md', 'design-full.md',
|
|
19
|
+
'design-frontend.md', 'scenarios.md', 'task.md', 'project-guidelines.md'];
|
|
20
|
+
const LAUNCHER_TEMPLATES = ['bridge.cjs', 'hook.sh', 'hook.cmd', 'hook.ps1'];
|
|
21
|
+
|
|
22
|
+
function launcherContent(name, node, entry) {
|
|
23
|
+
const template = fs.readFileSync(path.join(templateRoot, 'launcher', name), 'utf8');
|
|
24
|
+
const sh = value => value.replace(/'/g, "'\\''");
|
|
25
|
+
const cmd = value => value.replace(/%/g, '%%');
|
|
26
|
+
const ps = value => value.replace(/'/g, "''");
|
|
27
|
+
if (name === 'hook.sh') return template.replaceAll('__FIXED_NODE__', sh(node))
|
|
28
|
+
.replaceAll('__FIXED_ENTRY__', sh(entry));
|
|
29
|
+
if (name === 'hook.cmd') return template.replaceAll('__FIXED_NODE__', cmd(node))
|
|
30
|
+
.replaceAll('__FIXED_ENTRY__', cmd(entry));
|
|
31
|
+
if (name === 'hook.ps1') return template.replaceAll('__FIXED_NODE__', ps(node))
|
|
32
|
+
.replaceAll('__FIXED_ENTRY__', ps(entry));
|
|
33
|
+
return template;
|
|
34
|
+
}
|
|
35
|
+
function installed(root) {
|
|
36
|
+
return Object.entries(adapters.PLATFORMS).filter(([name, value]) => {
|
|
37
|
+
const text = f.readText(f.inside(root, value.hooks));
|
|
38
|
+
if (text === null) return false;
|
|
39
|
+
if (name === 'opencode') return adapters.owned(text);
|
|
40
|
+
const data = f.object(JSON.parse(text), 'platform config');
|
|
41
|
+
return Object.values(data.hooks ?? {}).some(groups => Array.isArray(groups) &&
|
|
42
|
+
groups.some(group => Array.isArray(group.hooks) && group.hooks.some(adapters.managed)));
|
|
43
|
+
}).map(([name]) => name);
|
|
44
|
+
}
|
|
45
|
+
function add(plans, conflicts, root, relative, after, managed = false) {
|
|
46
|
+
const file = f.inside(root, relative), before = f.readText(file);
|
|
47
|
+
if (managed && before !== null && !adapters.owned(before)) {
|
|
48
|
+
conflicts.push({ path: relative, code: 'file_unmanaged',
|
|
49
|
+
message: 'Preserve/remove unmanaged file before init' });
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (before !== after) plans.set(relative, { file, path: relative, before, after });
|
|
53
|
+
}
|
|
54
|
+
function attempt(conflicts, relative, operation) {
|
|
55
|
+
try { operation(); } catch (error) {
|
|
56
|
+
conflicts.push({ path: relative, code: error.code || 'init_file_failed', message: error.message });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function platformPlan(plans, conflicts, root, platform, node, entry) {
|
|
60
|
+
const target = adapters.PLATFORMS[platform];
|
|
61
|
+
const hookFile = f.inside(root, target.hooks);
|
|
62
|
+
const before = f.readText(hookFile);
|
|
63
|
+
if (platform === 'opencode') {
|
|
64
|
+
add(plans, conflicts, root, target.hooks, adapters.plugin(node, entry), true);
|
|
65
|
+
add(plans, conflicts, root, '.opencode/plugins/code-flow/package.json',
|
|
66
|
+
JSON.stringify({ private: true, type: 'module', managed: adapters.MARKER }, null, 2) + '\n', true);
|
|
67
|
+
}
|
|
68
|
+
else attempt(conflicts, target.hooks, () => add(plans, conflicts, root, target.hooks,
|
|
69
|
+
adapters.mergeHooks(before, platform, node, entry)));
|
|
70
|
+
const instruction = fs.readFileSync(path.join(templateRoot, 'instructions.md'), 'utf8');
|
|
71
|
+
attempt(conflicts, target.instructions, () => add(plans, conflicts, root, target.instructions,
|
|
72
|
+
adapters.instructions(f.readText(f.inside(root, target.instructions)), instruction)));
|
|
73
|
+
for (const skill of SKILLS) {
|
|
74
|
+
const name = platform === 'codex' ? skill.name : skill.path;
|
|
75
|
+
const relative = target.skills + '/' + name + (platform === 'codex' ? '/SKILL.md' : '.md');
|
|
76
|
+
add(plans, conflicts, root, relative,
|
|
77
|
+
fs.readFileSync(path.join(templateRoot, 'skills', skill.name + '.md'), 'utf8'), true);
|
|
78
|
+
}
|
|
79
|
+
if (platform === 'codex') {
|
|
80
|
+
const relative = '.codex/config.toml';
|
|
81
|
+
attempt(conflicts, relative, () => add(plans, conflicts, root, relative,
|
|
82
|
+
adapters.codexConfig(f.readText(f.inside(root, relative)))));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function plan(root, options = {}) {
|
|
86
|
+
const dataDir = f.inside(root, '.code-flow'), configuration = f.inside(root, '.code-flow/config.json');
|
|
87
|
+
const before = f.readText(configuration, 65536);
|
|
88
|
+
f.demand(before !== null || !fs.existsSync(dataDir) || fs.readdirSync(dataDir).length === 0,
|
|
89
|
+
'directory_format', 'Nonempty .code-flow without v1 config; preserve it outside the project before init');
|
|
90
|
+
if (before !== null) config.load(root);
|
|
91
|
+
const platforms = installed(root);
|
|
92
|
+
if (options.platform === 'all') {
|
|
93
|
+
for (const name of Object.keys(adapters.PLATFORMS)) if (!platforms.includes(name)) platforms.push(name);
|
|
94
|
+
} else if (options.platform) {
|
|
95
|
+
f.demand(Object.hasOwn(adapters.PLATFORMS, options.platform), 'platform_invalid', 'Unknown platform: ' + options.platform);
|
|
96
|
+
if (!platforms.includes(options.platform)) platforms.push(options.platform);
|
|
97
|
+
}
|
|
98
|
+
f.demand(platforms.length > 0, 'platform_required', 'First init requires --platform');
|
|
99
|
+
const plans = new Map(), conflicts = [];
|
|
100
|
+
if (before === null) add(plans, conflicts, root, '.code-flow/config.json',
|
|
101
|
+
fs.readFileSync(path.join(templateRoot, 'config.json'), 'utf8'));
|
|
102
|
+
for (const name of WORKFLOW_TEMPLATES) {
|
|
103
|
+
add(plans, conflicts, root, '.code-flow/templates/' + name,
|
|
104
|
+
fs.readFileSync(path.join(templateRoot, 'workflow', name), 'utf8'));
|
|
105
|
+
}
|
|
106
|
+
const node = options.node ?? process.execPath, entry = options.entry ?? path.resolve(__dirname, '../bin/code-flow.js');
|
|
107
|
+
for (const name of LAUNCHER_TEMPLATES) {
|
|
108
|
+
add(plans, conflicts, root, '.code-flow/launcher/' + name, launcherContent(name, node, entry));
|
|
109
|
+
}
|
|
110
|
+
for (const platform of platforms.sort()) platformPlan(plans, conflicts, root, platform, node, entry);
|
|
111
|
+
return { plans: [...plans.values()], platforms, conflicts };
|
|
112
|
+
}
|
|
113
|
+
async function init(root, options = {}) {
|
|
114
|
+
const build = () => plan(root, options);
|
|
115
|
+
const execute = prepared => {
|
|
116
|
+
if (!options.dryRun) {
|
|
117
|
+
for (const item of prepared.plans) f.atomicWrite(item.file, item.after, item.before);
|
|
118
|
+
for (const directory of ['.code-flow/specs', '.code-flow/tasks',
|
|
119
|
+
'.code-flow/templates', '.code-flow/launcher'])
|
|
120
|
+
fs.mkdirSync(f.inside(root, directory), { recursive: true });
|
|
121
|
+
}
|
|
122
|
+
return { ok: prepared.conflicts.length === 0, action: 'init', dry_run: Boolean(options.dryRun),
|
|
123
|
+
platforms: prepared.platforms, conflicts: prepared.conflicts,
|
|
124
|
+
changes: prepared.plans.map(item => ({ path: item.path,
|
|
125
|
+
action: item.before === null ? 'create' : 'update' })),
|
|
126
|
+
next: prepared.conflicts.length ? 'Resolve reported file conflicts, then re-run init.' :
|
|
127
|
+
prepared.platforms.includes('codex') ? 'Review/trust changed hooks in Codex /hooks.' : 'Reload platform configuration.' };
|
|
128
|
+
};
|
|
129
|
+
if (options.dryRun) return execute(build());
|
|
130
|
+
return f.withLock(root, 'write', async () => execute(build()));
|
|
131
|
+
}
|
|
132
|
+
module.exports = { init, plan, installed, WORKFLOW_TEMPLATES, LAUNCHER_TEMPLATES };
|
package/lib/learn.js
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
const f = require('./fs');
|
|
4
|
+
const fm = require('./frontmatter');
|
|
5
|
+
const spec = require('./spec');
|
|
6
|
+
const { reference } = require('./config');
|
|
7
|
+
const markdown = require('./markdown');
|
|
8
|
+
function normalize(text) { return text.replace(/\r\n/g, '\n').trim(); }
|
|
9
|
+
function validate(delta) {
|
|
10
|
+
f.object(delta, 'Delta');
|
|
11
|
+
f.demand(typeof delta.id === 'string' && /^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(delta.id), 'delta_id', 'Invalid Delta ID');
|
|
12
|
+
f.demand(['add', 'update', 'remove'].includes(delta.op), 'delta_op', 'Invalid Delta operation');
|
|
13
|
+
const target = reference(delta.target);
|
|
14
|
+
f.demand(!target.rule, 'delta_target', 'Delta target must be a Spec path');
|
|
15
|
+
const result = { id: delta.id, op: delta.op, target: target.id,
|
|
16
|
+
evidence: normalize(f.string(delta.evidence, 'Delta evidence')) };
|
|
17
|
+
if (delta.op !== 'remove') result.rule = normalize(f.string(delta.rule, 'Delta rule'));
|
|
18
|
+
if (delta.op !== 'add') {
|
|
19
|
+
f.string(delta.ref, 'Delta ref');
|
|
20
|
+
f.demand(/^RULE-[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*$/.test(delta.ref), 'delta_ref', 'Invalid Delta Rule reference');
|
|
21
|
+
result.ref = delta.ref; result.before = normalize(f.string(delta.before, 'Delta before'));
|
|
22
|
+
}
|
|
23
|
+
return result;
|
|
24
|
+
}
|
|
25
|
+
function deltas(task) {
|
|
26
|
+
const source = task.specDelta ?? [];
|
|
27
|
+
f.demand(Array.isArray(source), 'delta_invalid', 'Spec Delta must be an array');
|
|
28
|
+
const ids = new Set();
|
|
29
|
+
return source.map(item => {
|
|
30
|
+
const delta = validate(item);
|
|
31
|
+
f.demand(!ids.has(delta.id), 'delta_duplicate', 'Duplicate Delta ID: ' + delta.id); ids.add(delta.id);
|
|
32
|
+
return delta;
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
function section(body, name) {
|
|
36
|
+
const lines = body.replace(/\r\n/g, '\n').split('\n');
|
|
37
|
+
let start = -1, fence = null;
|
|
38
|
+
for (let i = 0; i < lines.length; i++) {
|
|
39
|
+
const marker = markdown.fenceMarker(lines[i], fence);
|
|
40
|
+
if (marker) {
|
|
41
|
+
fence = marker.next;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (fence) continue;
|
|
45
|
+
if (lines[i] === '## ' + name && start === -1) { start = i; continue; }
|
|
46
|
+
if (start !== -1 && /^## /.test(lines[i])) return { lines, start, end: i };
|
|
47
|
+
}
|
|
48
|
+
return { lines, start, end: lines.length };
|
|
49
|
+
}
|
|
50
|
+
function records(body) {
|
|
51
|
+
const area = section(body, 'Applied Deltas');
|
|
52
|
+
if (area.start < 0) return [];
|
|
53
|
+
return area.lines.slice(area.start + 1, area.end).filter(line => line.startsWith('- ')).map(line => {
|
|
54
|
+
let record;
|
|
55
|
+
try { record = JSON.parse(line.slice(2)); } catch (error) {
|
|
56
|
+
throw new f.FlowError('delta_history', 'Invalid Applied Deltas JSON: ' + error.message);
|
|
57
|
+
}
|
|
58
|
+
f.object(record, 'Applied Delta');
|
|
59
|
+
f.string(record.source, 'Applied Delta source');
|
|
60
|
+
f.demand(/^RULE-[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*$/.test(record.rule) &&
|
|
61
|
+
/^[a-f0-9]{64}$/.test(record.payload_sha256), 'delta_history', 'Invalid Applied Delta record');
|
|
62
|
+
return record;
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
function appendSection(body, name, addition) {
|
|
66
|
+
const area = section(body, name), lines = area.lines;
|
|
67
|
+
if (area.start < 0) return body.replace(/\s*$/, '') + '\n\n## ' + name + '\n\n' + addition + '\n';
|
|
68
|
+
lines.splice(area.end, 0, addition, '');
|
|
69
|
+
return lines.join('\n');
|
|
70
|
+
}
|
|
71
|
+
function ruleBlock(id, text) {
|
|
72
|
+
return normalize(text).split('\n').map((line, index) => index ? (line ? ' ' + line : '') : '- [' + id + '] ' + line).join('\n');
|
|
73
|
+
}
|
|
74
|
+
function mutate(document, delta, history) {
|
|
75
|
+
const parsed = spec.parseRules(document.body), meta = { ...document.meta };
|
|
76
|
+
const existing = parsed.rules.find(rule => rule.id === delta.ref);
|
|
77
|
+
if (delta.op !== 'add') {
|
|
78
|
+
f.demand(existing && normalize(existing.text) === delta.before, 'delta_precondition',
|
|
79
|
+
'Rule changed or missing: ' + delta.target + '#' + delta.ref, 1);
|
|
80
|
+
const lines = parsed.lines;
|
|
81
|
+
lines.splice(existing.start, existing.end - existing.start,
|
|
82
|
+
...(delta.op === 'remove' ? [] : [ruleBlock(existing.id, delta.rule), '']));
|
|
83
|
+
return { body: lines.join('\n'), meta, rule: existing.id, status: 'applied' };
|
|
84
|
+
}
|
|
85
|
+
const duplicate = parsed.rules.find(rule => normalize(rule.text) === delta.rule);
|
|
86
|
+
if (duplicate) return { body: document.body, meta, rule: duplicate.id, status: 'duplicate' };
|
|
87
|
+
const used = [...parsed.rules.map(rule => rule.id), ...history.map(item => item.rule)];
|
|
88
|
+
f.demand(!used.some(id => /^RULE-\d+$/.test(id) && Number(id.slice(5)) >= meta.next_rule),
|
|
89
|
+
'rule_counter', 'next_rule would reuse an allocated Rule ID', 1);
|
|
90
|
+
f.demand(Number.isSafeInteger(meta.next_rule + 1), 'rule_counter', 'next_rule exhausted', 1);
|
|
91
|
+
const id = 'RULE-' + String(meta.next_rule).padStart(3, '0'); meta.next_rule++;
|
|
92
|
+
return { body: appendSection(document.body, 'Rules', ruleBlock(id, delta.rule)), meta, rule: id, status: 'applied' };
|
|
93
|
+
}
|
|
94
|
+
function applyDelta(document, delta, source, globalHistory) {
|
|
95
|
+
const targetHistory = records(document.body);
|
|
96
|
+
const payload = f.hash(JSON.stringify(delta));
|
|
97
|
+
const previous = globalHistory.get(source) ?? [];
|
|
98
|
+
f.demand(previous.length <= 1, 'delta_history', 'Duplicate Applied Delta identity: ' + source, 1);
|
|
99
|
+
if (previous.length) {
|
|
100
|
+
f.demand(previous[0].record.payload_sha256 === payload, 'delta_conflict',
|
|
101
|
+
'Applied Delta payload changed: ' + source, 1);
|
|
102
|
+
return { document, result: { source, target: delta.target, rule: previous[0].record.rule,
|
|
103
|
+
status: 'already-applied' } };
|
|
104
|
+
}
|
|
105
|
+
const mutation = mutate({ ...document, body: document.body.replace(/\r\n/g, '\n') }, delta, targetHistory);
|
|
106
|
+
const record = { source, op: delta.op, rule: mutation.rule, payload_sha256: payload, at: new Date().toISOString() };
|
|
107
|
+
const body = appendSection(mutation.body, 'Applied Deltas', '- ' + JSON.stringify(record));
|
|
108
|
+
spec.parseRules(body);
|
|
109
|
+
globalHistory.set(source, [{ record, target: document.id }]);
|
|
110
|
+
const after = fm.stringify(document, mutation.meta, body.replace(/\n/g, document.eol));
|
|
111
|
+
const updated = { ...fm.parse(after), id: document.id, file: document.file };
|
|
112
|
+
return { document: updated, result: { source, target: delta.target, rule: mutation.rule, status: mutation.status } };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function loadHistory(root) {
|
|
116
|
+
const base = path.join(root, '.code-flow/specs'), history = new Map();
|
|
117
|
+
for (const relative of spec.walk(base)) {
|
|
118
|
+
const id = relative.slice(0, -3), document = spec.load(root, id);
|
|
119
|
+
for (const record of records(document.body)) {
|
|
120
|
+
const list = history.get(record.source) ?? [];
|
|
121
|
+
list.push({ record, target: id });
|
|
122
|
+
history.set(record.source, list);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return history;
|
|
126
|
+
}
|
|
127
|
+
function plan(root, tasks) {
|
|
128
|
+
const changes = new Map(), results = [], sources = new Map(), history = loadHistory(root);
|
|
129
|
+
const ordered = [...tasks].sort((a, b) =>
|
|
130
|
+
a.relative.localeCompare(b.relative) || a.id.localeCompare(b.id));
|
|
131
|
+
for (const task of ordered) {
|
|
132
|
+
f.demand(task.status === 'done' && !task.relative.startsWith('.code-flow/tasks/archived/'),
|
|
133
|
+
'learn_state', 'Learning requires an unarchived done Task', 1);
|
|
134
|
+
sources.set(task.relative, { file: task.document.file, raw: task.document.raw });
|
|
135
|
+
for (const delta of deltas(task)) {
|
|
136
|
+
if (!changes.has(delta.target)) {
|
|
137
|
+
const loaded = spec.load(root, delta.target);
|
|
138
|
+
changes.set(delta.target, { before: loaded.raw, document: loaded });
|
|
139
|
+
}
|
|
140
|
+
const item = changes.get(delta.target);
|
|
141
|
+
const source = task.relative.slice('.code-flow/tasks/'.length) + '#' + task.id + '#' + delta.id;
|
|
142
|
+
const applied = applyDelta(item.document, delta, source, history);
|
|
143
|
+
item.document = applied.document; results.push(applied.result);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return { changes: [...changes.values()].sort((a, b) => a.document.id.localeCompare(b.document.id)), results,
|
|
147
|
+
sources: [...sources.values()] };
|
|
148
|
+
}
|
|
149
|
+
function checkSources(prepared) {
|
|
150
|
+
for (const item of prepared.sources) f.demand(f.readText(item.file) === item.raw,
|
|
151
|
+
'concurrent_change', 'Task changed during learning: ' + item.file, 1);
|
|
152
|
+
}
|
|
153
|
+
function commit(prepared, writer = f.atomicWrite) {
|
|
154
|
+
checkSources(prepared);
|
|
155
|
+
for (const item of prepared.changes) {
|
|
156
|
+
checkSources(prepared);
|
|
157
|
+
if (item.document.raw !== item.before) writer(item.document.file, item.document.raw, item.before);
|
|
158
|
+
}
|
|
159
|
+
return { ok: true, action: 'learn', results: prepared.results };
|
|
160
|
+
}
|
|
161
|
+
function preview(prepared) {
|
|
162
|
+
return { ok: true, action: 'learn', dry_run: true, results: prepared.results,
|
|
163
|
+
changes: prepared.changes.filter(item => item.before !== item.document.raw)
|
|
164
|
+
.map(item => ({ file: item.document.file, before: item.before, after: item.document.raw })) };
|
|
165
|
+
}
|
|
166
|
+
async function apply(root, documents, dryRun = false) {
|
|
167
|
+
if (dryRun) return preview(plan(root, documents));
|
|
168
|
+
return f.withLock(root, 'write', async () => commit(plan(root, documents)));
|
|
169
|
+
}
|
|
170
|
+
module.exports = { validate, deltas, plan, commit, preview, apply, records, normalize };
|
package/lib/locks.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const f = require('./fs');
|
|
5
|
+
const repo = require('./repo');
|
|
6
|
+
|
|
7
|
+
const bases = new Map();
|
|
8
|
+
|
|
9
|
+
function base(root) {
|
|
10
|
+
const real = f.realpath(root);
|
|
11
|
+
if (bases.has(real)) return bases.get(real);
|
|
12
|
+
const identity = repo.identity(real);
|
|
13
|
+
const value = identity.git ? identity.git_common_dir : real;
|
|
14
|
+
bases.set(real, value);
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function directory(root) {
|
|
19
|
+
return path.join(f.transientRoot(base(root)), 'locks');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function withRepository(root, operation) {
|
|
23
|
+
return f.withLockIn(directory(root), 'repository', operation);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function withRepositorySync(root, operation) {
|
|
27
|
+
const lock = f.acquireIn(directory(root), 'repository');
|
|
28
|
+
try { return operation(); } finally { f.releaseLock(lock); }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function acquireGroup(root, group, timeoutSeconds, signal) {
|
|
32
|
+
const deadline = Date.now() + timeoutSeconds * 1000;
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
const attempt = () => {
|
|
35
|
+
if (signal.aborted) { reject(Object.assign(new Error('cancelled'), { code: 'cancelled' })); return; }
|
|
36
|
+
try { resolve(f.acquireIn(directory(root), 'group:' + group)); return; } catch (error) {
|
|
37
|
+
if (error.code !== 'locked') { reject(error); return; }
|
|
38
|
+
}
|
|
39
|
+
if (Date.now() >= deadline) {
|
|
40
|
+
reject(Object.assign(new Error('group lock timeout: ' + group), { code: 'lock_timeout' })); return;
|
|
41
|
+
}
|
|
42
|
+
setTimeout(attempt, 25);
|
|
43
|
+
};
|
|
44
|
+
attempt();
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function release(lock) {
|
|
49
|
+
f.releaseLock(lock);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = { base, directory, withRepository, withRepositorySync, acquireGroup, release };
|
package/lib/markdown.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function fenceMarker(line, current) {
|
|
4
|
+
const match = /^\s*(`{3,}|~{3,})/.exec(line);
|
|
5
|
+
if (!match) return null;
|
|
6
|
+
const token = match[1];
|
|
7
|
+
if (!current) return { next: token };
|
|
8
|
+
const closes = token[0] === current[0] && token.length >= current.length &&
|
|
9
|
+
line.slice(match[0].length).trim() === '';
|
|
10
|
+
return { next: closes ? null : current };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
module.exports = { fenceMarker };
|
package/lib/process.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const crypto = require('node:crypto');
|
|
5
|
+
const { spawnSync } = require('node:child_process');
|
|
6
|
+
const config = require('./config');
|
|
7
|
+
const task = require('./task');
|
|
8
|
+
const spec = require('./spec');
|
|
9
|
+
const doctor = require('./doctor');
|
|
10
|
+
|
|
11
|
+
function stamp() {
|
|
12
|
+
return new Date().toISOString().replace(/[:.]/g, '-');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function write(file, text) {
|
|
16
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
17
|
+
fs.writeFileSync(file, text);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function json(file, value) {
|
|
21
|
+
write(file, JSON.stringify(value, null, 2) + '\n');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function walk(directory, prefix = '') {
|
|
25
|
+
if (!fs.existsSync(directory)) return [];
|
|
26
|
+
return fs.readdirSync(directory, { withFileTypes: true })
|
|
27
|
+
.sort((left, right) => left.name.localeCompare(right.name))
|
|
28
|
+
.flatMap(entry => {
|
|
29
|
+
const relative = prefix + entry.name;
|
|
30
|
+
return entry.isDirectory() ? walk(path.join(directory, entry.name), relative + '/') : [relative];
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function copyTree(source, target) {
|
|
35
|
+
if (fs.existsSync(source)) fs.cpSync(source, target, { recursive: true });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function git(root, args) {
|
|
39
|
+
const result = spawnSync('git', args, { cwd: root, encoding: 'utf8', maxBuffer: 8 * 1024 * 1024 });
|
|
40
|
+
return result.status === 0 ? result.stdout : 'git unavailable\n';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function safe(operation) {
|
|
44
|
+
try { return operation(); } catch (error) {
|
|
45
|
+
return { error: { code: error.code || 'operation_failed', message: error.message } };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const README = `code-flow process bundle
|
|
50
|
+
|
|
51
|
+
events.jsonl optional CLI/Hook event log (command, args, exit, ms, byte counts, compact summary)
|
|
52
|
+
task-status.json code-flow task status --json
|
|
53
|
+
doctor.json code-flow doctor --json
|
|
54
|
+
spec-list.json code-flow spec list --json
|
|
55
|
+
code-flow/specs/ project Spec snapshot
|
|
56
|
+
code-flow/tasks/ project task/requirement document snapshot
|
|
57
|
+
code-flow/config.json project config
|
|
58
|
+
files.sha256 SHA-256 of Specs and task documents
|
|
59
|
+
git-status.txt working tree status
|
|
60
|
+
git-log.txt recent commits
|
|
61
|
+
git-diff-stat.txt uncommitted diff summary
|
|
62
|
+
transcript.jsonl optional host conversation transcript
|
|
63
|
+
|
|
64
|
+
Privacy: this bundle can contain project paths, requirement text, and conversation
|
|
65
|
+
content. Review it before sharing. The event log does not store rule text or source code.
|
|
66
|
+
|
|
67
|
+
Analysis targets: command sequence and timing, finish retries, blocked states,
|
|
68
|
+
injected/omitted rules and byte counts, spec learn/archive results, task status,
|
|
69
|
+
and the requirement documents produced by the workflow.
|
|
70
|
+
`;
|
|
71
|
+
|
|
72
|
+
function collect(root, options = {}) {
|
|
73
|
+
const settings = config.load(root);
|
|
74
|
+
const out = options.out ? path.resolve(options.out) :
|
|
75
|
+
path.join(root, settings.capture.path, 'bundle-' + stamp());
|
|
76
|
+
fs.mkdirSync(out, { recursive: true });
|
|
77
|
+
json(path.join(out, 'task-status.json'), safe(() => task.list(root)));
|
|
78
|
+
json(path.join(out, 'doctor.json'), safe(() => doctor.run(root)));
|
|
79
|
+
json(path.join(out, 'spec-list.json'), safe(() => spec.list(root)));
|
|
80
|
+
const events = process.env.CF_EVENT_LOG || path.join(root, settings.capture.path, 'events.jsonl');
|
|
81
|
+
if (fs.existsSync(events)) fs.copyFileSync(events, path.join(out, 'events.jsonl'));
|
|
82
|
+
copyTree(path.join(root, '.code-flow/specs'), path.join(out, 'code-flow/specs'));
|
|
83
|
+
copyTree(path.join(root, '.code-flow/tasks'), path.join(out, 'code-flow/tasks'));
|
|
84
|
+
if (fs.existsSync(path.join(root, '.code-flow/config.json'))) {
|
|
85
|
+
fs.copyFileSync(path.join(root, '.code-flow/config.json'), path.join(out, 'code-flow/config.json'));
|
|
86
|
+
}
|
|
87
|
+
const documents = [];
|
|
88
|
+
for (const name of ['specs', 'tasks']) {
|
|
89
|
+
const base = path.join(root, '.code-flow', name);
|
|
90
|
+
for (const relative of walk(base)) if (relative.endsWith('.md')) documents.push([base, relative]);
|
|
91
|
+
}
|
|
92
|
+
const lines = documents.sort(([leftBase, left], [rightBase, right]) =>
|
|
93
|
+
(leftBase + '/' + left).localeCompare(rightBase + '/' + right)).map(([base, relative]) => {
|
|
94
|
+
const file = path.join(base, relative);
|
|
95
|
+
const digest = crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
|
96
|
+
return digest + ' ' + path.relative(root, file).split(path.sep).join('/');
|
|
97
|
+
});
|
|
98
|
+
write(path.join(out, 'files.sha256'), lines.join('\n') + (lines.length ? '\n' : ''));
|
|
99
|
+
for (const [name, args] of [['git-status.txt', ['status', '--short']],
|
|
100
|
+
['git-log.txt', ['log', '--oneline', '-20']], ['git-diff-stat.txt', ['diff', '--stat']]]) {
|
|
101
|
+
write(path.join(out, name), git(root, args));
|
|
102
|
+
}
|
|
103
|
+
let transcript = false;
|
|
104
|
+
if (options.transcript) {
|
|
105
|
+
const file = path.resolve(options.transcript);
|
|
106
|
+
if (fs.existsSync(file)) {
|
|
107
|
+
fs.copyFileSync(file, path.join(out, 'transcript.jsonl'));
|
|
108
|
+
transcript = true;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
write(path.join(out, 'README.txt'), README);
|
|
112
|
+
return { ok: true, action: 'process collect', out, transcript,
|
|
113
|
+
text: 'Process bundle: ' + out + '\n' };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
module.exports = { collect };
|