shift-ax 0.3.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/LICENSE +21 -0
- package/README.ko.md +145 -0
- package/README.md +143 -0
- package/dist/adapters/claude-code/adapter.js +90 -0
- package/dist/adapters/codex/adapter.js +94 -0
- package/dist/adapters/contracts.js +7 -0
- package/dist/adapters/index.js +12 -0
- package/dist/core/context/context-bundle.js +300 -0
- package/dist/core/context/discovery.js +82 -0
- package/dist/core/context/global-index-authoring.js +199 -0
- package/dist/core/context/global-knowledge-updates.js +116 -0
- package/dist/core/context/glossary.js +73 -0
- package/dist/core/context/guided-onboarding.js +233 -0
- package/dist/core/context/index-authoring.js +47 -0
- package/dist/core/context/index-resolver.js +78 -0
- package/dist/core/context/onboarding.js +186 -0
- package/dist/core/diagnostics/doctor.js +154 -0
- package/dist/core/finalization/commit-message.js +76 -0
- package/dist/core/finalization/commit-workflow.js +131 -0
- package/dist/core/memory/consolidation.js +99 -0
- package/dist/core/memory/decision-register.js +141 -0
- package/dist/core/memory/entity-memory.js +25 -0
- package/dist/core/memory/learned-debug.js +52 -0
- package/dist/core/memory/summary-checkpoints.js +9 -0
- package/dist/core/memory/thread-promotion.js +22 -0
- package/dist/core/memory/threads.js +66 -0
- package/dist/core/memory/topic-recall.js +52 -0
- package/dist/core/observability/context-health.js +15 -0
- package/dist/core/observability/context-monitor.js +29 -0
- package/dist/core/observability/state-handoff.js +78 -0
- package/dist/core/observability/topic-status.js +40 -0
- package/dist/core/observability/topics-status.js +26 -0
- package/dist/core/observability/verification-debt.js +82 -0
- package/dist/core/planning/brainstorm.js +120 -0
- package/dist/core/planning/escalation.js +69 -0
- package/dist/core/planning/execution-handoff.js +61 -0
- package/dist/core/planning/execution-launch.js +156 -0
- package/dist/core/planning/execution-orchestrator.js +87 -0
- package/dist/core/planning/feedback-reactions.js +75 -0
- package/dist/core/planning/lifecycle-events.js +45 -0
- package/dist/core/planning/plan-review.js +76 -0
- package/dist/core/planning/policy-context-sync.js +154 -0
- package/dist/core/planning/request-pipeline.js +386 -0
- package/dist/core/planning/workflow-state.js +18 -0
- package/dist/core/policies/project-profile.js +28 -0
- package/dist/core/policies/team-preferences.js +17 -0
- package/dist/core/review/aggregate-reviews.js +129 -0
- package/dist/core/review/run-lanes.js +376 -0
- package/dist/core/settings/global-context-home.js +28 -0
- package/dist/core/settings/project-settings.js +37 -0
- package/dist/core/shell/platform-shell.js +144 -0
- package/dist/core/topics/bootstrap.js +119 -0
- package/dist/core/topics/topic-artifacts.js +36 -0
- package/dist/core/topics/worktree-runtime.js +141 -0
- package/dist/core/topics/worktree.js +8 -0
- package/dist/platform/claude-code/bootstrap.js +66 -0
- package/dist/platform/claude-code/execution.js +157 -0
- package/dist/platform/claude-code/scaffold/CLAUDE.template.md +40 -0
- package/dist/platform/claude-code/scaffold/commands/doctor.template.md +11 -0
- package/dist/platform/claude-code/scaffold/commands/export-context.template.md +20 -0
- package/dist/platform/claude-code/scaffold/commands/onboard.template.md +43 -0
- package/dist/platform/claude-code/scaffold/commands/onboarding.template.md +43 -0
- package/dist/platform/claude-code/scaffold/commands/request.template.md +19 -0
- package/dist/platform/claude-code/scaffold/commands/resume.template.md +12 -0
- package/dist/platform/claude-code/scaffold/commands/review.template.md +10 -0
- package/dist/platform/claude-code/scaffold/commands/status.template.md +14 -0
- package/dist/platform/claude-code/scaffold/commands/topics.template.md +10 -0
- package/dist/platform/claude-code/scaffold/hooks/shift-ax-session-start.template.md +29 -0
- package/dist/platform/claude-code/tmux.js +35 -0
- package/dist/platform/claude-code/upstream/tmux/imported/detached-session.js +40 -0
- package/dist/platform/claude-code/upstream/tmux/imported/session-name.js +19 -0
- package/dist/platform/claude-code/upstream/worktree/imported/get-worktree-root.js +39 -0
- package/dist/platform/claude-code/upstream/worktree/imported/managed-worktree.js +77 -0
- package/dist/platform/claude-code/worktree.js +79 -0
- package/dist/platform/codex/bootstrap.js +69 -0
- package/dist/platform/codex/execution.js +163 -0
- package/dist/platform/codex/scaffold/AGENTS.template.md +40 -0
- package/dist/platform/codex/scaffold/prompts/doctor.template.md +11 -0
- package/dist/platform/codex/scaffold/prompts/export-context.template.md +20 -0
- package/dist/platform/codex/scaffold/prompts/onboard.template.md +43 -0
- package/dist/platform/codex/scaffold/prompts/onboarding.template.md +43 -0
- package/dist/platform/codex/scaffold/prompts/request.template.md +19 -0
- package/dist/platform/codex/scaffold/prompts/resume.template.md +14 -0
- package/dist/platform/codex/scaffold/prompts/review.template.md +10 -0
- package/dist/platform/codex/scaffold/prompts/shift-ax-bootstrap.template.md +23 -0
- package/dist/platform/codex/scaffold/prompts/status.template.md +14 -0
- package/dist/platform/codex/scaffold/prompts/topics.template.md +10 -0
- package/dist/platform/codex/scaffold/skills/doctor/SKILL.template.md +11 -0
- package/dist/platform/codex/scaffold/skills/export-context/SKILL.template.md +20 -0
- package/dist/platform/codex/scaffold/skills/onboard/SKILL.template.md +43 -0
- package/dist/platform/codex/scaffold/skills/request/SKILL.template.md +19 -0
- package/dist/platform/codex/scaffold/skills/resume/SKILL.template.md +14 -0
- package/dist/platform/codex/scaffold/skills/review/SKILL.template.md +10 -0
- package/dist/platform/codex/scaffold/skills/status/SKILL.template.md +14 -0
- package/dist/platform/codex/scaffold/skills/topics/SKILL.template.md +10 -0
- package/dist/platform/codex/tmux.js +45 -0
- package/dist/platform/codex/upstream/tmux/imported/resize-hook-registration.js +37 -0
- package/dist/platform/codex/upstream/tmux/imported/resize-hooks.js +29 -0
- package/dist/platform/codex/upstream/tmux/imported/sanitize-team-name.js +18 -0
- package/dist/platform/codex/upstream/worktree/imported/managed-worktree.js +208 -0
- package/dist/platform/codex/upstream/worktree/imported/resolve-repo-root.js +14 -0
- package/dist/platform/codex/worktree.js +99 -0
- package/dist/platform/index.js +10 -0
- package/dist/platform/product-shell-commands.js +17 -0
- package/dist/platform/scaffold.js +16 -0
- package/dist/platform/upstream-imports.js +5 -0
- package/dist/scripts/ax-approve-plan.js +30 -0
- package/dist/scripts/ax-bootstrap-assets.js +19 -0
- package/dist/scripts/ax-bootstrap-topic.js +24 -0
- package/dist/scripts/ax-build-context-bundle.js +35 -0
- package/dist/scripts/ax-checkpoint-context.js +22 -0
- package/dist/scripts/ax-consolidate-memory.js +7 -0
- package/dist/scripts/ax-context-health.js +26 -0
- package/dist/scripts/ax-decisions.js +32 -0
- package/dist/scripts/ax-doctor.js +25 -0
- package/dist/scripts/ax-entity-memory.js +19 -0
- package/dist/scripts/ax-export-context.js +8 -0
- package/dist/scripts/ax-finalize-commit.js +23 -0
- package/dist/scripts/ax-init-context.js +41 -0
- package/dist/scripts/ax-launch-execution.js +24 -0
- package/dist/scripts/ax-learned-debug-save.js +30 -0
- package/dist/scripts/ax-learned-debug.js +12 -0
- package/dist/scripts/ax-monitor-context.js +28 -0
- package/dist/scripts/ax-onboard-context.js +112 -0
- package/dist/scripts/ax-pause-work.js +33 -0
- package/dist/scripts/ax-platform-manifest.js +19 -0
- package/dist/scripts/ax-promote-thread.js +20 -0
- package/dist/scripts/ax-react-feedback.js +28 -0
- package/dist/scripts/ax-recall-topics.js +20 -0
- package/dist/scripts/ax-recall.js +58 -0
- package/dist/scripts/ax-refresh-state.js +15 -0
- package/dist/scripts/ax-resolve-context.js +34 -0
- package/dist/scripts/ax-review.js +24 -0
- package/dist/scripts/ax-run-request.js +198 -0
- package/dist/scripts/ax-scaffold-build.js +19 -0
- package/dist/scripts/ax-shell.js +123 -0
- package/dist/scripts/ax-sync-policy-context.js +40 -0
- package/dist/scripts/ax-team-preferences.js +20 -0
- package/dist/scripts/ax-thread-save.js +26 -0
- package/dist/scripts/ax-threads.js +11 -0
- package/dist/scripts/ax-topic-status.js +18 -0
- package/dist/scripts/ax-topics-status.js +22 -0
- package/dist/scripts/ax-verification-debt.js +22 -0
- package/dist/scripts/ax-worktree-create.js +22 -0
- package/dist/scripts/ax-worktree-plan.js +18 -0
- package/dist/scripts/ax-worktree-remove.js +18 -0
- package/dist/scripts/ax.js +132 -0
- package/package.json +71 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { topicArtifactPath } from '../topics/topic-artifacts.js';
|
|
4
|
+
async function readArtifact(path) {
|
|
5
|
+
return readFile(path, 'utf8');
|
|
6
|
+
}
|
|
7
|
+
async function assertResolvedContextReady(topicDir) {
|
|
8
|
+
const raw = await readArtifact(topicArtifactPath(topicDir, 'resolved_context')).catch(() => '');
|
|
9
|
+
if (!raw) {
|
|
10
|
+
throw new Error('resolved context artifact is missing');
|
|
11
|
+
}
|
|
12
|
+
const parsed = JSON.parse(raw);
|
|
13
|
+
if ((parsed.unresolved_paths ?? []).length > 0) {
|
|
14
|
+
throw new Error('resolved context still has unresolved base-context paths');
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export async function readExecutionHandoff(topicDir) {
|
|
18
|
+
const raw = await readArtifact(topicArtifactPath(topicDir, 'execution_handoff'));
|
|
19
|
+
return JSON.parse(raw);
|
|
20
|
+
}
|
|
21
|
+
export async function readExecutionWorktreePath(topicDir) {
|
|
22
|
+
const raw = await readArtifact(topicArtifactPath(topicDir, 'worktree_state'));
|
|
23
|
+
const parsed = JSON.parse(raw);
|
|
24
|
+
if (!parsed.worktree_path) {
|
|
25
|
+
throw new Error('worktree-state.json does not contain worktree_path');
|
|
26
|
+
}
|
|
27
|
+
return parsed.worktree_path;
|
|
28
|
+
}
|
|
29
|
+
function parseMarkdownSections(markdown) {
|
|
30
|
+
const sections = new Map();
|
|
31
|
+
const lines = String(markdown || '').split(/\r?\n/);
|
|
32
|
+
let currentSection = null;
|
|
33
|
+
let buffer = [];
|
|
34
|
+
const flush = () => {
|
|
35
|
+
if (!currentSection)
|
|
36
|
+
return;
|
|
37
|
+
sections.set(currentSection, buffer.join('\n').trim());
|
|
38
|
+
};
|
|
39
|
+
for (const line of lines) {
|
|
40
|
+
const heading = line.match(/^##\s+(.+?)\s*$/);
|
|
41
|
+
if (heading) {
|
|
42
|
+
flush();
|
|
43
|
+
currentSection = heading[1].trim();
|
|
44
|
+
buffer = [];
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (currentSection) {
|
|
48
|
+
buffer.push(line);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
flush();
|
|
52
|
+
return sections;
|
|
53
|
+
}
|
|
54
|
+
function bulletizeSection(content, fallback) {
|
|
55
|
+
const items = String(content || '')
|
|
56
|
+
.split(/\r?\n/)
|
|
57
|
+
.map((line) => line.trim())
|
|
58
|
+
.filter(Boolean)
|
|
59
|
+
.map((line) => line.replace(/^[-*]\s+/, '').trim())
|
|
60
|
+
.filter(Boolean)
|
|
61
|
+
.map((line) => (line.startsWith('- ') ? line : `- ${line}`));
|
|
62
|
+
if (items.length > 0)
|
|
63
|
+
return items;
|
|
64
|
+
return fallback ? [`- ${fallback}`] : [];
|
|
65
|
+
}
|
|
66
|
+
function buildExecutionPromptContent(input) {
|
|
67
|
+
const brainstormSections = parseMarkdownSections(input.brainstorm);
|
|
68
|
+
const specSections = parseMarkdownSections(input.spec);
|
|
69
|
+
const relevantDocs = (input.resolvedContext.matches ?? [])
|
|
70
|
+
.map((match) => {
|
|
71
|
+
const label = match.label?.trim();
|
|
72
|
+
const path = match.path?.trim();
|
|
73
|
+
if (!label || !path)
|
|
74
|
+
return null;
|
|
75
|
+
return `- ${label} -> ${path}`;
|
|
76
|
+
})
|
|
77
|
+
.filter((item) => Boolean(item));
|
|
78
|
+
const constraints = bulletizeSection(specSections.get('Constraints') || brainstormSections.get('Constraints'), 'No extra constraints were recorded.');
|
|
79
|
+
const outOfScope = bulletizeSection(specSections.get('Out of Scope') || brainstormSections.get('Out of Scope'), 'No out-of-scope items were recorded.');
|
|
80
|
+
const verification = bulletizeSection(specSections.get('Verification Expectations') || brainstormSections.get('Verification Expectations'), 'No explicit verification expectations were recorded.');
|
|
81
|
+
return [
|
|
82
|
+
`You are executing Shift AX task ${input.task.id} inside this worktree: ${input.worktreePath}`,
|
|
83
|
+
'',
|
|
84
|
+
`Request summary: ${input.summary.trim() || input.request.trim()}`,
|
|
85
|
+
'',
|
|
86
|
+
`Do this task now: ${input.task.source_text.trim()}`,
|
|
87
|
+
'',
|
|
88
|
+
'Relevant base-context docs to consult first if they apply:',
|
|
89
|
+
...(relevantDocs.length > 0 ? relevantDocs : ['- No directly matched base-context docs were recorded.']),
|
|
90
|
+
'',
|
|
91
|
+
'Constraints:',
|
|
92
|
+
...constraints,
|
|
93
|
+
'',
|
|
94
|
+
'Out of scope:',
|
|
95
|
+
...outOfScope,
|
|
96
|
+
'',
|
|
97
|
+
'Verification expectations:',
|
|
98
|
+
...verification,
|
|
99
|
+
'',
|
|
100
|
+
'Execution rules:',
|
|
101
|
+
'- Read any listed base-context docs before editing when they are relevant.',
|
|
102
|
+
'- Make the file edits now; do not stop at an explanation or plan.',
|
|
103
|
+
'- Keep changes inside the assigned worktree only.',
|
|
104
|
+
'- Do not widen scope beyond the assigned task.',
|
|
105
|
+
'- If you cannot complete the edit, say exactly why.',
|
|
106
|
+
'',
|
|
107
|
+
`Routing reason: ${input.task.reason.trim()}`,
|
|
108
|
+
'',
|
|
109
|
+
'In your final response, briefly list changed files and tests run.',
|
|
110
|
+
'',
|
|
111
|
+
].join('\n');
|
|
112
|
+
}
|
|
113
|
+
export async function materializeExecutionPrompts(topicDir, taskId) {
|
|
114
|
+
await assertResolvedContextReady(topicDir);
|
|
115
|
+
const [handoff, worktreePath, request, summary, brainstorm, spec, plan, resolvedContextRaw] = await Promise.all([
|
|
116
|
+
readExecutionHandoff(topicDir),
|
|
117
|
+
readExecutionWorktreePath(topicDir),
|
|
118
|
+
readArtifact(topicArtifactPath(topicDir, 'request')),
|
|
119
|
+
readArtifact(topicArtifactPath(topicDir, 'request_summary')),
|
|
120
|
+
readArtifact(topicArtifactPath(topicDir, 'brainstorm')),
|
|
121
|
+
readArtifact(topicArtifactPath(topicDir, 'spec')),
|
|
122
|
+
readArtifact(topicArtifactPath(topicDir, 'implementation_plan')),
|
|
123
|
+
readArtifact(topicArtifactPath(topicDir, 'resolved_context')),
|
|
124
|
+
]);
|
|
125
|
+
const resolvedContext = JSON.parse(resolvedContextRaw);
|
|
126
|
+
const tasks = taskId
|
|
127
|
+
? handoff.tasks.filter((task) => task.id === taskId)
|
|
128
|
+
: handoff.tasks;
|
|
129
|
+
if (tasks.length === 0) {
|
|
130
|
+
throw new Error(taskId ? `No execution task found for ${taskId}` : 'No execution tasks found');
|
|
131
|
+
}
|
|
132
|
+
const artifacts = [];
|
|
133
|
+
for (const task of tasks) {
|
|
134
|
+
const promptPath = join(topicDir, 'execution-prompts', `${task.id}.md`);
|
|
135
|
+
const outputPath = join(topicDir, 'execution-results', `${task.id}.json`);
|
|
136
|
+
await mkdir(dirname(promptPath), { recursive: true });
|
|
137
|
+
await mkdir(dirname(outputPath), { recursive: true });
|
|
138
|
+
await writeFile(promptPath, `${buildExecutionPromptContent({
|
|
139
|
+
request,
|
|
140
|
+
summary,
|
|
141
|
+
brainstorm,
|
|
142
|
+
spec,
|
|
143
|
+
plan,
|
|
144
|
+
resolvedContext,
|
|
145
|
+
task,
|
|
146
|
+
worktreePath,
|
|
147
|
+
}).trimEnd()}\n`, 'utf8');
|
|
148
|
+
artifacts.push({
|
|
149
|
+
task,
|
|
150
|
+
worktreePath,
|
|
151
|
+
promptPath,
|
|
152
|
+
outputPath,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
return artifacts;
|
|
156
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { mkdir, stat, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import { topicArtifactPath } from '../topics/topic-artifacts.js';
|
|
4
|
+
async function pathExists(path) {
|
|
5
|
+
try {
|
|
6
|
+
await stat(path);
|
|
7
|
+
return true;
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
async function hasUsableOutput(path) {
|
|
14
|
+
try {
|
|
15
|
+
const details = await stat(path);
|
|
16
|
+
return details.size > 0;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
async function waitForOutput(path, pollIntervalMs, timeoutMs) {
|
|
23
|
+
const startedAt = Date.now();
|
|
24
|
+
while (Date.now() - startedAt <= timeoutMs) {
|
|
25
|
+
if (await pathExists(path))
|
|
26
|
+
return;
|
|
27
|
+
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
28
|
+
}
|
|
29
|
+
throw new Error(`Timed out waiting for execution output: ${path}`);
|
|
30
|
+
}
|
|
31
|
+
async function writeExecutionState(topicDir, state) {
|
|
32
|
+
await writeFile(topicArtifactPath(topicDir, 'execution_state'), `${JSON.stringify(state, null, 2)}\n`, 'utf8');
|
|
33
|
+
}
|
|
34
|
+
export async function orchestrateExecutionTasks({ topicDir, tasks, runTask, pollIntervalMs = 50, timeoutMs = 60_000, }) {
|
|
35
|
+
const startedAt = new Date().toISOString();
|
|
36
|
+
const taskStates = [];
|
|
37
|
+
for (const task of tasks) {
|
|
38
|
+
const taskStartedAt = new Date().toISOString();
|
|
39
|
+
try {
|
|
40
|
+
await mkdir(dirname(task.output_path), { recursive: true });
|
|
41
|
+
if (!(await hasUsableOutput(task.output_path))) {
|
|
42
|
+
await runTask(task);
|
|
43
|
+
await waitForOutput(task.output_path, pollIntervalMs, timeoutMs);
|
|
44
|
+
}
|
|
45
|
+
taskStates.push({
|
|
46
|
+
task_id: task.task_id,
|
|
47
|
+
execution_mode: task.execution_mode,
|
|
48
|
+
status: 'completed',
|
|
49
|
+
output_path: task.output_path,
|
|
50
|
+
...(task.session_name ? { session_name: task.session_name } : {}),
|
|
51
|
+
started_at: taskStartedAt,
|
|
52
|
+
completed_at: new Date().toISOString(),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
57
|
+
taskStates.push({
|
|
58
|
+
task_id: task.task_id,
|
|
59
|
+
execution_mode: task.execution_mode,
|
|
60
|
+
status: /timed out/i.test(message) ? 'timed_out' : 'failed',
|
|
61
|
+
output_path: task.output_path,
|
|
62
|
+
...(task.session_name ? { session_name: task.session_name } : {}),
|
|
63
|
+
started_at: taskStartedAt,
|
|
64
|
+
completed_at: new Date().toISOString(),
|
|
65
|
+
error: message,
|
|
66
|
+
});
|
|
67
|
+
const failedState = {
|
|
68
|
+
version: 1,
|
|
69
|
+
overall_status: 'failed',
|
|
70
|
+
started_at: startedAt,
|
|
71
|
+
completed_at: new Date().toISOString(),
|
|
72
|
+
tasks: taskStates,
|
|
73
|
+
};
|
|
74
|
+
await writeExecutionState(topicDir, failedState);
|
|
75
|
+
return failedState;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const state = {
|
|
79
|
+
version: 1,
|
|
80
|
+
overall_status: 'completed',
|
|
81
|
+
started_at: startedAt,
|
|
82
|
+
completed_at: new Date().toISOString(),
|
|
83
|
+
tasks: taskStates,
|
|
84
|
+
};
|
|
85
|
+
await writeExecutionState(topicDir, state);
|
|
86
|
+
return state;
|
|
87
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { writeFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { renderReviewSummaryMarkdown } from '../review/aggregate-reviews.js';
|
|
4
|
+
import { readWorkflowState, writeWorkflowState } from './workflow-state.js';
|
|
5
|
+
import { recordLifecycleEvent } from './lifecycle-events.js';
|
|
6
|
+
function buildFeedbackVerdict({ kind, summary, now, }) {
|
|
7
|
+
return {
|
|
8
|
+
version: 1,
|
|
9
|
+
lane: 'downstream-feedback',
|
|
10
|
+
status: kind === 'ci-failed' ? 'blocked' : 'changes_requested',
|
|
11
|
+
checked_at: now.toISOString(),
|
|
12
|
+
summary,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
function buildFeedbackAggregate(verdict) {
|
|
16
|
+
return {
|
|
17
|
+
version: 1,
|
|
18
|
+
overall_status: verdict.status,
|
|
19
|
+
commit_allowed: false,
|
|
20
|
+
next_stage: 'implementation',
|
|
21
|
+
required_lanes: [],
|
|
22
|
+
approved_lanes: [],
|
|
23
|
+
changes_requested_lanes: verdict.status === 'changes_requested' ? ['downstream-feedback'] : [],
|
|
24
|
+
blocked_lanes: verdict.status === 'blocked' ? ['downstream-feedback'] : [],
|
|
25
|
+
missing_lanes: [],
|
|
26
|
+
verdicts: [verdict],
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
async function writeFeedbackArtifacts(topicDir, verdict, aggregate) {
|
|
30
|
+
const reviewDir = join(topicDir, 'review');
|
|
31
|
+
await Promise.all([
|
|
32
|
+
writeFile(join(reviewDir, 'downstream-feedback.json'), `${JSON.stringify(verdict, null, 2)}\n`, 'utf8'),
|
|
33
|
+
writeFile(join(reviewDir, 'aggregate.json'), `${JSON.stringify(aggregate, null, 2)}\n`, 'utf8'),
|
|
34
|
+
writeFile(join(reviewDir, 'summary.md'), renderReviewSummaryMarkdown(aggregate), 'utf8'),
|
|
35
|
+
]);
|
|
36
|
+
}
|
|
37
|
+
export async function applyFeedbackReaction({ topicDir, kind, summary, now = new Date(), }) {
|
|
38
|
+
if (!summary.trim()) {
|
|
39
|
+
throw new Error('feedback summary is required');
|
|
40
|
+
}
|
|
41
|
+
const workflow = await readWorkflowState(topicDir);
|
|
42
|
+
workflow.phase = 'implementation_running';
|
|
43
|
+
workflow.updated_at = now.toISOString();
|
|
44
|
+
workflow.review = {
|
|
45
|
+
overall_status: kind === 'ci-failed' ? 'blocked' : 'changes_requested',
|
|
46
|
+
commit_allowed: false,
|
|
47
|
+
next_stage: 'implementation',
|
|
48
|
+
};
|
|
49
|
+
delete workflow.verification;
|
|
50
|
+
await writeWorkflowState(topicDir, workflow);
|
|
51
|
+
const verdict = buildFeedbackVerdict({
|
|
52
|
+
kind,
|
|
53
|
+
summary: summary.trim(),
|
|
54
|
+
now,
|
|
55
|
+
});
|
|
56
|
+
const aggregate = buildFeedbackAggregate(verdict);
|
|
57
|
+
await writeFeedbackArtifacts(topicDir, verdict, aggregate);
|
|
58
|
+
await recordLifecycleEvent({
|
|
59
|
+
topicDir,
|
|
60
|
+
phase: workflow.phase,
|
|
61
|
+
event: 'feedback.received',
|
|
62
|
+
summary: summary.trim(),
|
|
63
|
+
reaction: {
|
|
64
|
+
key: kind,
|
|
65
|
+
action: 'reopen_execution',
|
|
66
|
+
outcome: verdict.status,
|
|
67
|
+
},
|
|
68
|
+
now,
|
|
69
|
+
});
|
|
70
|
+
return {
|
|
71
|
+
workflow,
|
|
72
|
+
aggregate,
|
|
73
|
+
feedback_verdict: verdict,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
function lifecycleLogPath(topicDir) {
|
|
4
|
+
return join(topicDir, 'lifecycle-log.json');
|
|
5
|
+
}
|
|
6
|
+
function reactionLogPath(topicDir) {
|
|
7
|
+
return join(topicDir, 'reaction-log.json');
|
|
8
|
+
}
|
|
9
|
+
async function readJsonArray(path) {
|
|
10
|
+
try {
|
|
11
|
+
const raw = await readFile(path, 'utf8');
|
|
12
|
+
return JSON.parse(raw);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return [];
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
async function writeJsonArray(path, value) {
|
|
19
|
+
await mkdir(dirname(path), { recursive: true });
|
|
20
|
+
await writeFile(path, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
|
21
|
+
}
|
|
22
|
+
export async function recordLifecycleEvent({ topicDir, phase, event, summary, reaction, now = new Date(), }) {
|
|
23
|
+
const events = await readJsonArray(lifecycleLogPath(topicDir));
|
|
24
|
+
events.push({
|
|
25
|
+
phase,
|
|
26
|
+
event,
|
|
27
|
+
summary,
|
|
28
|
+
recorded_at: now.toISOString(),
|
|
29
|
+
});
|
|
30
|
+
await writeJsonArray(lifecycleLogPath(topicDir), events);
|
|
31
|
+
if (reaction) {
|
|
32
|
+
const reactions = await readJsonArray(reactionLogPath(topicDir));
|
|
33
|
+
reactions.push({
|
|
34
|
+
...reaction,
|
|
35
|
+
recorded_at: now.toISOString(),
|
|
36
|
+
});
|
|
37
|
+
await writeJsonArray(reactionLogPath(topicDir), reactions);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export async function readLifecycleEvents(topicDir) {
|
|
41
|
+
return readJsonArray(lifecycleLogPath(topicDir));
|
|
42
|
+
}
|
|
43
|
+
export async function readReactionRecords(topicDir) {
|
|
44
|
+
return readJsonArray(reactionLogPath(topicDir));
|
|
45
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { topicArtifactPath } from '../topics/topic-artifacts.js';
|
|
4
|
+
import { recordLifecycleEvent } from './lifecycle-events.js';
|
|
5
|
+
import { readPolicyContextSyncArtifact } from './policy-context-sync.js';
|
|
6
|
+
import { readWorkflowState, writeWorkflowState, } from './workflow-state.js';
|
|
7
|
+
function sha256(value) {
|
|
8
|
+
return createHash('sha256').update(value).digest('hex');
|
|
9
|
+
}
|
|
10
|
+
async function readPlanFile(topicDir) {
|
|
11
|
+
return readFile(topicArtifactPath(topicDir, 'implementation_plan'), 'utf8');
|
|
12
|
+
}
|
|
13
|
+
export async function readPlanReviewArtifact(topicDir) {
|
|
14
|
+
const raw = await readFile(topicArtifactPath(topicDir, 'plan_review'), 'utf8');
|
|
15
|
+
return JSON.parse(raw);
|
|
16
|
+
}
|
|
17
|
+
export async function writePlanReviewArtifact(topicDir, artifact) {
|
|
18
|
+
await writeFile(topicArtifactPath(topicDir, 'plan_review'), `${JSON.stringify(artifact, null, 2)}\n`, 'utf8');
|
|
19
|
+
}
|
|
20
|
+
export async function recordPlanReviewDecision({ topicDir, reviewer, status, notes, now = new Date(), }) {
|
|
21
|
+
const planContent = await readPlanFile(topicDir);
|
|
22
|
+
const fingerprint = status === 'approved'
|
|
23
|
+
? {
|
|
24
|
+
plan_path: 'implementation-plan.md',
|
|
25
|
+
sha256: sha256(planContent),
|
|
26
|
+
}
|
|
27
|
+
: undefined;
|
|
28
|
+
const artifact = {
|
|
29
|
+
version: 1,
|
|
30
|
+
status,
|
|
31
|
+
reviewer,
|
|
32
|
+
reviewed_at: now.toISOString(),
|
|
33
|
+
...(notes ? { notes } : {}),
|
|
34
|
+
...(fingerprint ? { approved_plan_fingerprint: fingerprint } : {}),
|
|
35
|
+
};
|
|
36
|
+
await writePlanReviewArtifact(topicDir, artifact);
|
|
37
|
+
const workflow = await readWorkflowState(topicDir);
|
|
38
|
+
workflow.plan_review_status = status;
|
|
39
|
+
const policyContextSync = await readPolicyContextSyncArtifact(topicDir);
|
|
40
|
+
workflow.phase =
|
|
41
|
+
status === 'approved'
|
|
42
|
+
? policyContextSync.status === 'required'
|
|
43
|
+
? 'awaiting_policy_sync'
|
|
44
|
+
: 'approved'
|
|
45
|
+
: 'awaiting_plan_review';
|
|
46
|
+
workflow.updated_at = now.toISOString();
|
|
47
|
+
await writeWorkflowState(topicDir, workflow);
|
|
48
|
+
if (status === 'approved' && policyContextSync.status === 'required') {
|
|
49
|
+
await recordLifecycleEvent({
|
|
50
|
+
topicDir,
|
|
51
|
+
phase: workflow.phase,
|
|
52
|
+
event: 'policy.sync_required',
|
|
53
|
+
summary: 'Plan approval requires shared base-context policy updates before implementation can start.',
|
|
54
|
+
now,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
return artifact;
|
|
58
|
+
}
|
|
59
|
+
export async function verifyApprovedPlanFingerprint({ topicDir, }) {
|
|
60
|
+
const review = await readPlanReviewArtifact(topicDir);
|
|
61
|
+
if (review.status !== 'approved' || !review.approved_plan_fingerprint) {
|
|
62
|
+
return {
|
|
63
|
+
matches: false,
|
|
64
|
+
reason: 'plan review is not approved',
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const currentSha = sha256(await readPlanFile(topicDir));
|
|
68
|
+
return {
|
|
69
|
+
matches: currentSha === review.approved_plan_fingerprint.sha256,
|
|
70
|
+
expected: review.approved_plan_fingerprint,
|
|
71
|
+
actual_sha256: currentSha,
|
|
72
|
+
...(currentSha === review.approved_plan_fingerprint.sha256
|
|
73
|
+
? {}
|
|
74
|
+
: { reason: 'approved plan fingerprint does not match current plan' }),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { authorBaseContextIndex } from '../context/index-authoring.js';
|
|
5
|
+
import { readProjectProfile, writeProjectProfile, } from '../policies/project-profile.js';
|
|
6
|
+
import { getRootDirFromTopicDir, topicArtifactPath } from '../topics/topic-artifacts.js';
|
|
7
|
+
import { recordLifecycleEvent } from './lifecycle-events.js';
|
|
8
|
+
import { readWorkflowState, writeWorkflowState } from './workflow-state.js';
|
|
9
|
+
const POLICY_UPDATE_HEADING = /^#{2,}\s+(?:base-context\s+policy\s+updates?|base-context\s+updates?|policy\s+updates?)\s*$/i;
|
|
10
|
+
const POLICY_UPDATE_NONE = /^\s*-\s*(none(?:\s+yet)?|no(?:\s+updates?)?|n\/a|not needed|없음|해당 없음)\s*\.?\s*$/i;
|
|
11
|
+
function dedupeStrings(values) {
|
|
12
|
+
return [...new Set(values.map((value) => value.trim()).filter(Boolean))];
|
|
13
|
+
}
|
|
14
|
+
function dedupeEntries(entries) {
|
|
15
|
+
const seen = new Set();
|
|
16
|
+
const result = [];
|
|
17
|
+
for (const entry of entries) {
|
|
18
|
+
const normalized = {
|
|
19
|
+
label: entry.label.trim(),
|
|
20
|
+
path: entry.path.trim(),
|
|
21
|
+
};
|
|
22
|
+
if (!normalized.label || !normalized.path)
|
|
23
|
+
continue;
|
|
24
|
+
const key = `${normalized.label}::${normalized.path}`;
|
|
25
|
+
if (seen.has(key))
|
|
26
|
+
continue;
|
|
27
|
+
seen.add(key);
|
|
28
|
+
result.push(normalized);
|
|
29
|
+
}
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
export function extractPolicyContextUpdates(markdown) {
|
|
33
|
+
const lines = String(markdown || '').split(/\r?\n/);
|
|
34
|
+
const items = [];
|
|
35
|
+
let capturing = false;
|
|
36
|
+
for (const line of lines) {
|
|
37
|
+
if (POLICY_UPDATE_HEADING.test(line.trim())) {
|
|
38
|
+
capturing = true;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (capturing && /^#{1,6}\s+/.test(line.trim())) {
|
|
42
|
+
capturing = false;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (!capturing)
|
|
46
|
+
continue;
|
|
47
|
+
if (POLICY_UPDATE_NONE.test(line)) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
const match = line.match(/^\s*-\s+(.+?)\s*$/);
|
|
51
|
+
if (match?.[1]) {
|
|
52
|
+
items.push(match[1].trim());
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return dedupeStrings(items);
|
|
56
|
+
}
|
|
57
|
+
export function inferPolicyContextSyncArtifact({ brainstormContent, specContent, implementationPlanContent, now = new Date(), }) {
|
|
58
|
+
const requiredUpdates = dedupeStrings([
|
|
59
|
+
...extractPolicyContextUpdates(brainstormContent),
|
|
60
|
+
...extractPolicyContextUpdates(specContent),
|
|
61
|
+
...extractPolicyContextUpdates(implementationPlanContent),
|
|
62
|
+
]);
|
|
63
|
+
return {
|
|
64
|
+
version: 1,
|
|
65
|
+
status: requiredUpdates.length > 0 ? 'required' : 'not_needed',
|
|
66
|
+
required_updates: requiredUpdates,
|
|
67
|
+
created_at: now.toISOString(),
|
|
68
|
+
updated_at: now.toISOString(),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
export async function writePolicyContextSyncArtifact(topicDir, artifact) {
|
|
72
|
+
await writeFile(topicArtifactPath(topicDir, 'policy_context_sync'), `${JSON.stringify(artifact, null, 2)}\n`, 'utf8');
|
|
73
|
+
}
|
|
74
|
+
export async function readPolicyContextSyncArtifact(topicDir) {
|
|
75
|
+
try {
|
|
76
|
+
const raw = await readFile(topicArtifactPath(topicDir, 'policy_context_sync'), 'utf8');
|
|
77
|
+
return JSON.parse(raw);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return {
|
|
81
|
+
version: 1,
|
|
82
|
+
status: 'not_needed',
|
|
83
|
+
required_updates: [],
|
|
84
|
+
created_at: new Date(0).toISOString(),
|
|
85
|
+
updated_at: new Date(0).toISOString(),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
async function mergeContextDocs(rootDir, entries) {
|
|
90
|
+
const mergedEntries = dedupeEntries(entries);
|
|
91
|
+
if (mergedEntries.length === 0)
|
|
92
|
+
return undefined;
|
|
93
|
+
const index = await authorBaseContextIndex({
|
|
94
|
+
rootDir,
|
|
95
|
+
entries: mergedEntries,
|
|
96
|
+
});
|
|
97
|
+
const profile = await readProjectProfile(rootDir);
|
|
98
|
+
if (profile) {
|
|
99
|
+
const byKey = new Map();
|
|
100
|
+
for (const doc of [...profile.context_docs, ...mergedEntries]) {
|
|
101
|
+
byKey.set(`${doc.label}::${doc.path}`, doc);
|
|
102
|
+
}
|
|
103
|
+
profile.context_docs = [...byKey.values()];
|
|
104
|
+
profile.updated_at = new Date().toISOString();
|
|
105
|
+
await writeProjectProfile(rootDir, profile);
|
|
106
|
+
}
|
|
107
|
+
return index;
|
|
108
|
+
}
|
|
109
|
+
export async function completePolicyContextSync({ topicDir, summary, syncedPaths = [], syncedEntries = [], now = new Date(), }) {
|
|
110
|
+
if (!summary.trim()) {
|
|
111
|
+
throw new Error('policy context sync summary is required');
|
|
112
|
+
}
|
|
113
|
+
const artifact = await readPolicyContextSyncArtifact(topicDir);
|
|
114
|
+
const rootDir = getRootDirFromTopicDir(topicDir);
|
|
115
|
+
const normalizedPaths = dedupeStrings(syncedPaths);
|
|
116
|
+
const normalizedEntries = dedupeEntries(syncedEntries);
|
|
117
|
+
if (artifact.status === 'required' && normalizedPaths.length === 0 && normalizedEntries.length === 0) {
|
|
118
|
+
throw new Error('policy context sync requires at least one updated path or index entry');
|
|
119
|
+
}
|
|
120
|
+
for (const path of normalizedPaths) {
|
|
121
|
+
if (!existsSync(join(rootDir, path))) {
|
|
122
|
+
throw new Error(`policy context path does not exist: ${path}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
for (const entry of normalizedEntries) {
|
|
126
|
+
if (!existsSync(join(rootDir, entry.path))) {
|
|
127
|
+
throw new Error(`policy context entry path does not exist: ${entry.path}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
await mergeContextDocs(rootDir, normalizedEntries);
|
|
131
|
+
const completed = {
|
|
132
|
+
...artifact,
|
|
133
|
+
status: artifact.status === 'not_needed' ? 'not_needed' : 'completed',
|
|
134
|
+
updated_at: now.toISOString(),
|
|
135
|
+
summary: summary.trim(),
|
|
136
|
+
synced_paths: normalizedPaths,
|
|
137
|
+
...(normalizedEntries.length > 0 ? { synced_entries: normalizedEntries } : {}),
|
|
138
|
+
};
|
|
139
|
+
await writePolicyContextSyncArtifact(topicDir, completed);
|
|
140
|
+
const workflow = await readWorkflowState(topicDir);
|
|
141
|
+
if (workflow.plan_review_status === 'approved') {
|
|
142
|
+
workflow.phase = 'approved';
|
|
143
|
+
workflow.updated_at = now.toISOString();
|
|
144
|
+
await writeWorkflowState(topicDir, workflow);
|
|
145
|
+
}
|
|
146
|
+
await recordLifecycleEvent({
|
|
147
|
+
topicDir,
|
|
148
|
+
phase: workflow.plan_review_status === 'approved' ? 'approved' : workflow.phase,
|
|
149
|
+
event: 'policy.sync_completed',
|
|
150
|
+
summary: summary.trim(),
|
|
151
|
+
now,
|
|
152
|
+
});
|
|
153
|
+
return completed;
|
|
154
|
+
}
|