@ai-content-space/loopx 0.1.1 → 0.1.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/README.md +343 -56
- package/README.zh-CN.md +392 -0
- package/package.json +4 -1
- package/plugins/loopx/.codex-plugin/plugin.json +1 -1
- package/plugins/loopx/scripts/plugin-install.test.mjs +1 -0
- package/plugins/loopx/skills/archive/SKILL.md +39 -0
- package/plugins/loopx/skills/build/SKILL.md +111 -9
- package/plugins/loopx/skills/clarify/SKILL.md +121 -1
- package/plugins/loopx/skills/debug/SKILL.md +296 -0
- package/plugins/loopx/skills/debug/condition-based-waiting.md +115 -0
- package/plugins/loopx/skills/debug/defense-in-depth.md +122 -0
- package/plugins/loopx/skills/debug/find-polluter.sh +63 -0
- package/plugins/loopx/skills/debug/root-cause-tracing.md +169 -0
- package/plugins/loopx/skills/go-style/SKILL.md +71 -0
- package/plugins/loopx/skills/kratos/SKILL.md +74 -0
- package/plugins/loopx/skills/kratos/references/advanced-features.md +314 -0
- package/plugins/loopx/skills/kratos/references/architecture.md +488 -0
- package/plugins/loopx/skills/kratos/references/configuration.md +399 -0
- package/plugins/loopx/skills/kratos/references/http-customization.md +512 -0
- package/plugins/loopx/skills/kratos/references/middleware-logging.md +400 -0
- package/plugins/loopx/skills/kratos/references/proto-api-design.md +432 -0
- package/plugins/loopx/skills/kratos/references/security-auth.md +411 -0
- package/plugins/loopx/skills/kratos/references/troubleshooting.md +385 -0
- package/plugins/loopx/skills/plan/SKILL.md +22 -2
- package/plugins/loopx/skills/review/SKILL.md +98 -1
- package/plugins/loopx/skills/tdd/SKILL.md +371 -0
- package/plugins/loopx/skills/tdd/testing-anti-patterns.md +299 -0
- package/plugins/loopx/skills/verify/SKILL.md +139 -0
- package/scripts/codex-stop-hook.mjs +71 -0
- package/scripts/codex-workflow-hook.mjs +153 -0
- package/skills/archive/SKILL.md +39 -0
- package/skills/build/SKILL.md +111 -9
- package/skills/clarify/SKILL.md +121 -1
- package/skills/debug/SKILL.md +296 -0
- package/skills/debug/condition-based-waiting.md +115 -0
- package/skills/debug/defense-in-depth.md +122 -0
- package/skills/debug/find-polluter.sh +63 -0
- package/skills/debug/root-cause-tracing.md +169 -0
- package/skills/go-style/SKILL.md +71 -0
- package/skills/kratos/SKILL.md +74 -0
- package/skills/kratos/references/advanced-features.md +314 -0
- package/skills/kratos/references/architecture.md +488 -0
- package/skills/kratos/references/configuration.md +399 -0
- package/skills/kratos/references/http-customization.md +512 -0
- package/skills/kratos/references/middleware-logging.md +400 -0
- package/skills/kratos/references/proto-api-design.md +432 -0
- package/skills/kratos/references/security-auth.md +411 -0
- package/skills/kratos/references/troubleshooting.md +385 -0
- package/skills/plan/SKILL.md +22 -2
- package/skills/review/SKILL.md +98 -1
- package/skills/tdd/SKILL.md +371 -0
- package/skills/tdd/testing-anti-patterns.md +299 -0
- package/skills/verify/SKILL.md +139 -0
- package/src/build-runtime.mjs +303 -26
- package/src/build-stop-gate.mjs +94 -0
- package/src/cli.mjs +51 -8
- package/src/codex-exec-runtime.mjs +105 -5
- package/src/context-manifest.mjs +172 -0
- package/src/install-discovery.mjs +352 -5
- package/src/next-skill.mjs +85 -0
- package/src/plan-runtime.mjs +100 -122
- package/src/review-runtime.mjs +378 -0
- package/src/runtime-maintenance.mjs +428 -14
- package/src/template-governance.mjs +223 -0
- package/src/workflow.mjs +1947 -118
- package/src/workspace-context.mjs +166 -0
- package/src/workspace-memory.mjs +69 -0
- package/templates/plan.md +6 -0
package/src/cli.mjs
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { autopilotStage, approveStage, buildStage, clarifyStage, initWorkspace, planStage, reviewStage, statusSummary } from './workflow.mjs';
|
|
3
|
+
import { archiveStage, autopilotStage, approveStage, buildStage, clarifyStage, initWorkspace, planStage, reviewStage, statusSummary } from './workflow.mjs';
|
|
4
4
|
import { installBundledSkills } from './install-discovery.mjs';
|
|
5
|
+
import { nextSkillCommand, withNextSkill } from './next-skill.mjs';
|
|
5
6
|
import { doctorRuntime, migrateLegacyRuntime } from './runtime-maintenance.mjs';
|
|
7
|
+
import { setupWorkspaceContext } from './workspace-context.mjs';
|
|
6
8
|
|
|
7
9
|
function usage() {
|
|
8
10
|
return [
|
|
@@ -12,9 +14,12 @@ function usage() {
|
|
|
12
14
|
' loopx approve <slug> --from <stage> --to <stage>',
|
|
13
15
|
' loopx plan [slug] [--direct <spec-path>] [--interactive] [--deliberate]',
|
|
14
16
|
' loopx build <slug> [--no-deslop]',
|
|
17
|
+
' loopx build --from-review <review-report-path> [--no-deslop]',
|
|
15
18
|
' loopx review <slug> [--reviewer <name>]',
|
|
19
|
+
' loopx archive <slug>',
|
|
16
20
|
' loopx autopilot <slug> [--reviewer <name>]',
|
|
17
21
|
' loopx status [slug] [--json]',
|
|
22
|
+
' loopx setup-context',
|
|
18
23
|
' loopx doctor',
|
|
19
24
|
' loopx migrate',
|
|
20
25
|
' loopx repair-install',
|
|
@@ -77,7 +82,7 @@ function printHumanStatus(status) {
|
|
|
77
82
|
console.log(`plan_deliberate_mode: ${status.state.plan_deliberate_mode}`);
|
|
78
83
|
console.log(`plan_architect_review_status: ${status.state.plan_architect_review_status}`);
|
|
79
84
|
console.log(`plan_critic_verdict: ${status.state.plan_critic_verdict}`);
|
|
80
|
-
console.log(`
|
|
85
|
+
console.log(`plan_artifact_status: ${status.state.plan_docs_status}`);
|
|
81
86
|
console.log(`plan_blockers: ${Array.isArray(status.state.plan_blockers) && status.state.plan_blockers.length > 0 ? status.state.plan_blockers.join(', ') : '(none)'}`);
|
|
82
87
|
}
|
|
83
88
|
if (status.state?.current_stage === 'build') {
|
|
@@ -87,8 +92,31 @@ function printHumanStatus(status) {
|
|
|
87
92
|
console.log(`build_architect_verification_status: ${status.state.build_architect_verification_status}`);
|
|
88
93
|
console.log(`build_deslop_status: ${status.state.build_deslop_status}`);
|
|
89
94
|
console.log(`build_regression_status: ${status.state.build_regression_status}`);
|
|
95
|
+
console.log(`context_manifest_status: ${status.state.context_manifest_status ?? 'unknown'}`);
|
|
90
96
|
console.log(`build_blockers: ${Array.isArray(status.state.build_blockers) && status.state.build_blockers.length > 0 ? status.state.build_blockers.join(', ') : '(none)'}`);
|
|
91
97
|
}
|
|
98
|
+
if (status.state?.workspace_journal_path) {
|
|
99
|
+
console.log(`workspace_journal_path: ${status.state.workspace_journal_path}`);
|
|
100
|
+
}
|
|
101
|
+
if (status.state?.change_artifacts_status) {
|
|
102
|
+
console.log(`change_artifacts_status: ${status.state.change_artifacts_status}`);
|
|
103
|
+
console.log(`spec_delta_status: ${status.state.spec_delta_status ?? 'unknown'}`);
|
|
104
|
+
console.log(`spec_sync_status: ${status.state.spec_sync_status ?? 'unknown'}`);
|
|
105
|
+
console.log(`archive_status: ${status.state.archive_status ?? 'unknown'}`);
|
|
106
|
+
}
|
|
107
|
+
if (status.state?.readiness && status.state?.authorization) {
|
|
108
|
+
for (const key of ['plan', 'build', 'review', 'done', 'archive']) {
|
|
109
|
+
if (status.state.readiness[key]) {
|
|
110
|
+
console.log(`readiness_${key}: ${status.state.readiness[key].ready}`);
|
|
111
|
+
}
|
|
112
|
+
if (status.state.authorization[key]) {
|
|
113
|
+
console.log(`authorization_${key}: ${status.state.authorization[key].authorized}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (status.hook) {
|
|
118
|
+
console.log(`hook_enabled: ${status.hook.enabled}`);
|
|
119
|
+
}
|
|
92
120
|
if (status.state?.autopilot_current_phase && status.state.autopilot_current_phase !== 'none') {
|
|
93
121
|
console.log(`autopilot_current_phase: ${status.state.autopilot_current_phase}`);
|
|
94
122
|
console.log(`autopilot_completed: ${status.state.autopilot_completed}`);
|
|
@@ -98,6 +126,10 @@ function printHumanStatus(status) {
|
|
|
98
126
|
console.log(`last_confirmed_transition: ${status.state?.last_confirmed_transition ?? 'none'}`);
|
|
99
127
|
console.log(`pending_user_decision: ${status.state?.pending_user_decision ?? 'none'}`);
|
|
100
128
|
console.log(`missing artifacts: ${status.missing_artifacts.length > 0 ? status.missing_artifacts.join(', ') : '(none)'}`);
|
|
129
|
+
const nextSkill = nextSkillCommand(status.state);
|
|
130
|
+
if (nextSkill) {
|
|
131
|
+
console.log(`next skill: ${nextSkill}`);
|
|
132
|
+
}
|
|
101
133
|
console.log(`next: ${status.next_action}`);
|
|
102
134
|
}
|
|
103
135
|
|
|
@@ -115,10 +147,15 @@ async function main() {
|
|
|
115
147
|
console.log(JSON.stringify({ ok: true, command, workspaceRoot: result.workspaceRoot, workflow: result.workflow?.state ?? null }, null, 2));
|
|
116
148
|
return;
|
|
117
149
|
}
|
|
150
|
+
case 'setup-context': {
|
|
151
|
+
const contextSetup = await setupWorkspaceContext(process.cwd());
|
|
152
|
+
console.log(JSON.stringify({ ok: true, command, contextSetup }, null, 2));
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
118
155
|
case 'clarify': {
|
|
119
156
|
const profile = options.get('--deep') ? 'deep' : 'standard';
|
|
120
157
|
const result = await clarifyStage(process.cwd(), positionals[0], { profile });
|
|
121
|
-
console.log(JSON.stringify({ ok: true, command, root: result.root, state: result.state }, null, 2));
|
|
158
|
+
console.log(JSON.stringify(withNextSkill({ ok: true, command, root: result.root, state: result.state }, result.state), null, 2));
|
|
122
159
|
return;
|
|
123
160
|
}
|
|
124
161
|
case 'approve': {
|
|
@@ -126,7 +163,7 @@ async function main() {
|
|
|
126
163
|
from: options.get('--from'),
|
|
127
164
|
to: options.get('--to'),
|
|
128
165
|
});
|
|
129
|
-
console.log(JSON.stringify({ ok: true, command, root: result.root, state: result.state }, null, 2));
|
|
166
|
+
console.log(JSON.stringify(withNextSkill({ ok: true, command, root: result.root, state: result.state }, result.state), null, 2));
|
|
130
167
|
return;
|
|
131
168
|
}
|
|
132
169
|
case 'plan': {
|
|
@@ -135,21 +172,27 @@ async function main() {
|
|
|
135
172
|
interactive: Boolean(options.get('--interactive')),
|
|
136
173
|
deliberate: Boolean(options.get('--deliberate')),
|
|
137
174
|
});
|
|
138
|
-
console.log(JSON.stringify({ ok: true, command, root: result.root, state: result.state }, null, 2));
|
|
175
|
+
console.log(JSON.stringify(withNextSkill({ ok: true, command, root: result.root, state: result.state }, result.state), null, 2));
|
|
139
176
|
return;
|
|
140
177
|
}
|
|
141
178
|
case 'build': {
|
|
142
|
-
const result = await buildStage(process.cwd(), positionals[0], {
|
|
179
|
+
const result = await buildStage(process.cwd(), options.get('--from-review') ? undefined : positionals[0], {
|
|
143
180
|
noDeslop: Boolean(options.get('--no-deslop')),
|
|
181
|
+
fromReviewPath: options.get('--from-review'),
|
|
144
182
|
});
|
|
145
|
-
console.log(JSON.stringify({ ok: true, command, root: result.root, state: result.state }, null, 2));
|
|
183
|
+
console.log(JSON.stringify(withNextSkill({ ok: true, command, root: result.root, state: result.state }, result.state), null, 2));
|
|
146
184
|
return;
|
|
147
185
|
}
|
|
148
186
|
case 'review': {
|
|
149
187
|
const result = await reviewStage(process.cwd(), positionals[0], {
|
|
150
188
|
reviewer: options.get('--reviewer') || 'independent-reviewer',
|
|
151
189
|
});
|
|
152
|
-
console.log(JSON.stringify({ ok: true, command, root: result.root, state: result.state, verdict: result.verdict }, null, 2));
|
|
190
|
+
console.log(JSON.stringify(withNextSkill({ ok: true, command, root: result.root, state: result.state, verdict: result.verdict, review_message_zh: result.reviewMessageZh }, result.state), null, 2));
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
case 'archive': {
|
|
194
|
+
const result = await archiveStage(process.cwd(), positionals[0]);
|
|
195
|
+
console.log(JSON.stringify({ ok: true, command, root: result.root, state: result.state }, null, 2));
|
|
153
196
|
return;
|
|
154
197
|
}
|
|
155
198
|
case 'autopilot': {
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
|
-
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { mkdtemp, readFile, rm, unlink, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
4
6
|
const DEFAULT_CODEX_MODEL = 'gpt-5.4';
|
|
5
7
|
const DEFAULT_CODEX_TIMEOUT_MS = 120000;
|
|
6
8
|
const DEFAULT_CODEX_REASONING = 'low';
|
|
9
|
+
const DIAGNOSTIC_TAIL_CHARS = 4000;
|
|
10
|
+
|
|
11
|
+
function diagnosticTail(value) {
|
|
12
|
+
return String(value || '').slice(-DIAGNOSTIC_TAIL_CHARS);
|
|
13
|
+
}
|
|
7
14
|
|
|
8
15
|
function codexBinary() {
|
|
9
16
|
return process.env.LOOPX_CODEX_BIN || 'codex';
|
|
@@ -22,7 +29,14 @@ export async function runCodexExec({
|
|
|
22
29
|
extraArgs = [],
|
|
23
30
|
timeoutMs = DEFAULT_CODEX_TIMEOUT_MS,
|
|
24
31
|
reasoningEffort = DEFAULT_CODEX_REASONING,
|
|
32
|
+
outputSchema = null,
|
|
33
|
+
promptViaStdin = false,
|
|
25
34
|
}) {
|
|
35
|
+
await unlink(outputPath).catch((error) => {
|
|
36
|
+
if (error?.code !== 'ENOENT') {
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
});
|
|
26
40
|
const args = [
|
|
27
41
|
'exec',
|
|
28
42
|
'--dangerously-bypass-approvals-and-sandbox',
|
|
@@ -34,8 +48,9 @@ export async function runCodexExec({
|
|
|
34
48
|
`model_reasoning_effort=\"${reasoningEffort}\"`,
|
|
35
49
|
'-o',
|
|
36
50
|
outputPath,
|
|
51
|
+
...(outputSchema ? ['--output-schema', outputSchema] : []),
|
|
37
52
|
...extraArgs,
|
|
38
|
-
prompt,
|
|
53
|
+
promptViaStdin ? '-' : prompt,
|
|
39
54
|
];
|
|
40
55
|
|
|
41
56
|
const child = spawn(codexBinary(), args, {
|
|
@@ -47,14 +62,16 @@ export async function runCodexExec({
|
|
|
47
62
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
48
63
|
});
|
|
49
64
|
|
|
50
|
-
child.stdin.end();
|
|
65
|
+
child.stdin.end(promptViaStdin ? prompt : undefined);
|
|
51
66
|
|
|
52
67
|
const stdoutChunks = [];
|
|
53
68
|
const stderrChunks = [];
|
|
54
69
|
child.stdout.on('data', (chunk) => stdoutChunks.push(Buffer.from(chunk)));
|
|
55
70
|
child.stderr.on('data', (chunk) => stderrChunks.push(Buffer.from(chunk)));
|
|
56
71
|
|
|
72
|
+
let timedOut = false;
|
|
57
73
|
const timeout = setTimeout(() => {
|
|
74
|
+
timedOut = true;
|
|
58
75
|
child.kill('SIGTERM');
|
|
59
76
|
}, timeoutMs);
|
|
60
77
|
|
|
@@ -67,6 +84,9 @@ export async function runCodexExec({
|
|
|
67
84
|
const stdout = Buffer.concat(stdoutChunks).toString('utf8');
|
|
68
85
|
const stderr = Buffer.concat(stderrChunks).toString('utf8');
|
|
69
86
|
const finalMessage = existsSync(outputPath) ? await readFile(outputPath, 'utf8') : '';
|
|
87
|
+
if (timedOut) {
|
|
88
|
+
throw new Error('timeout');
|
|
89
|
+
}
|
|
70
90
|
if (exitCode !== 0) {
|
|
71
91
|
throw new Error(`exit_${exitCode}`);
|
|
72
92
|
}
|
|
@@ -82,7 +102,7 @@ export async function runCodexExec({
|
|
|
82
102
|
const stderr = Buffer.concat(stderrChunks).toString('utf8');
|
|
83
103
|
const finalMessage = existsSync(outputPath) ? await readFile(outputPath, 'utf8') : '';
|
|
84
104
|
const message = error instanceof Error ? error.message : String(error);
|
|
85
|
-
throw new Error(`codex_exec_failed:${message}\nstdout:${stdout}\nstderr:${stderr}\nfinal:${finalMessage}`);
|
|
105
|
+
throw new Error(`codex_exec_failed:${message}\nstdout:${diagnosticTail(stdout)}\nstderr:${diagnosticTail(stderr)}\nfinal:${diagnosticTail(finalMessage)}`);
|
|
86
106
|
}
|
|
87
107
|
}
|
|
88
108
|
|
|
@@ -92,6 +112,86 @@ export async function runCodexExecJson(options) {
|
|
|
92
112
|
try {
|
|
93
113
|
return JSON.parse(text);
|
|
94
114
|
} catch (error) {
|
|
95
|
-
|
|
115
|
+
const stdout = diagnosticTail(result.stdout);
|
|
116
|
+
const stderr = diagnosticTail(result.stderr);
|
|
117
|
+
throw new Error(`codex_exec_invalid_json:${error instanceof Error ? error.message : String(error)}\nbody:${text}\nstdout:${stdout}\nstderr:${stderr}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export async function runCodexReviewJson({
|
|
122
|
+
cwd,
|
|
123
|
+
prompt,
|
|
124
|
+
outputPath,
|
|
125
|
+
model = DEFAULT_CODEX_MODEL,
|
|
126
|
+
timeoutMs = DEFAULT_CODEX_TIMEOUT_MS,
|
|
127
|
+
reasoningEffort = DEFAULT_CODEX_REASONING,
|
|
128
|
+
uncommitted = true,
|
|
129
|
+
outputSchema = null,
|
|
130
|
+
}) {
|
|
131
|
+
let schemaDir = null;
|
|
132
|
+
let schemaPath = outputSchema;
|
|
133
|
+
try {
|
|
134
|
+
if (!schemaPath) {
|
|
135
|
+
schemaDir = await mkdtemp(join(tmpdir(), 'loopx-code-review-schema-'));
|
|
136
|
+
schemaPath = join(schemaDir, 'schema.json');
|
|
137
|
+
await writeFile(schemaPath, JSON.stringify({
|
|
138
|
+
type: 'object',
|
|
139
|
+
additionalProperties: false,
|
|
140
|
+
required: ['status', 'verdict', 'summary', 'findings'],
|
|
141
|
+
properties: {
|
|
142
|
+
status: { enum: ['complete', 'skipped'] },
|
|
143
|
+
verdict: { enum: ['approve', 'request-changes'] },
|
|
144
|
+
summary: { type: 'string' },
|
|
145
|
+
rollbackTarget: {
|
|
146
|
+
anyOf: [
|
|
147
|
+
{ enum: ['build', 'plan', 'clarify'] },
|
|
148
|
+
{ type: 'null' },
|
|
149
|
+
],
|
|
150
|
+
},
|
|
151
|
+
findings: {
|
|
152
|
+
type: 'array',
|
|
153
|
+
items: {
|
|
154
|
+
type: 'object',
|
|
155
|
+
additionalProperties: false,
|
|
156
|
+
required: ['severity', 'file', 'line', 'message'],
|
|
157
|
+
properties: {
|
|
158
|
+
severity: { enum: ['high', 'medium', 'low'] },
|
|
159
|
+
file: { anyOf: [{ type: 'string' }, { type: 'null' }] },
|
|
160
|
+
line: { anyOf: [{ type: 'number' }, { type: 'null' }] },
|
|
161
|
+
message: { type: 'string' },
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
}));
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
return await runCodexExecJson({
|
|
170
|
+
cwd,
|
|
171
|
+
prompt,
|
|
172
|
+
outputPath,
|
|
173
|
+
model,
|
|
174
|
+
timeoutMs,
|
|
175
|
+
reasoningEffort,
|
|
176
|
+
outputSchema: schemaPath,
|
|
177
|
+
promptViaStdin: true,
|
|
178
|
+
extraArgs: uncommitted ? [] : [],
|
|
179
|
+
});
|
|
180
|
+
} catch (error) {
|
|
181
|
+
return await runCodexExecJson({
|
|
182
|
+
cwd,
|
|
183
|
+
prompt,
|
|
184
|
+
outputPath,
|
|
185
|
+
model,
|
|
186
|
+
timeoutMs,
|
|
187
|
+
reasoningEffort,
|
|
188
|
+
promptViaStdin: true,
|
|
189
|
+
extraArgs: uncommitted ? [] : [],
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
} finally {
|
|
193
|
+
if (schemaDir) {
|
|
194
|
+
await rm(schemaDir, { recursive: true, force: true });
|
|
195
|
+
}
|
|
96
196
|
}
|
|
97
197
|
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname, join, relative, resolve } from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { inspectWorkspaceContext, resolveWorkspaceContextPaths } from './workspace-context.mjs';
|
|
6
|
+
|
|
7
|
+
export const CONTEXT_MANIFEST_SCHEMA_VERSION = 1;
|
|
8
|
+
const MAX_MANIFEST_ROWS = 80;
|
|
9
|
+
|
|
10
|
+
function normalizePath(cwd, path) {
|
|
11
|
+
const resolved = resolve(path);
|
|
12
|
+
const rel = relative(cwd, resolved);
|
|
13
|
+
return rel && !rel.startsWith('..') ? rel : resolved;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function normalizeReason(reason) {
|
|
17
|
+
if (Array.isArray(reason)) {
|
|
18
|
+
return reason.filter(Boolean).map(String).join(',');
|
|
19
|
+
}
|
|
20
|
+
return String(reason || 'context');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function row(cwd, { stage, kind, path, reason, priority, required = true }) {
|
|
24
|
+
const normalizedPath = normalizePath(cwd, path);
|
|
25
|
+
return {
|
|
26
|
+
schema_version: CONTEXT_MANIFEST_SCHEMA_VERSION,
|
|
27
|
+
stage,
|
|
28
|
+
kind,
|
|
29
|
+
path: normalizedPath,
|
|
30
|
+
reason: normalizeReason(reason),
|
|
31
|
+
priority,
|
|
32
|
+
required: Boolean(required),
|
|
33
|
+
exists: existsSync(resolve(cwd, normalizedPath)),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function stableRows(rows) {
|
|
38
|
+
const byPath = new Map();
|
|
39
|
+
for (const item of rows) {
|
|
40
|
+
const key = `${item.stage}:${item.kind}:${item.path}`;
|
|
41
|
+
const existing = byPath.get(key);
|
|
42
|
+
if (!existing) {
|
|
43
|
+
byPath.set(key, item);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const reasons = new Set([
|
|
47
|
+
...existing.reason.split(',').map((value) => value.trim()).filter(Boolean),
|
|
48
|
+
...item.reason.split(',').map((value) => value.trim()).filter(Boolean),
|
|
49
|
+
]);
|
|
50
|
+
byPath.set(key, {
|
|
51
|
+
...existing,
|
|
52
|
+
priority: Math.min(existing.priority, item.priority),
|
|
53
|
+
required: existing.required || item.required,
|
|
54
|
+
reason: [...reasons].sort().join(','),
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
return [...byPath.values()]
|
|
58
|
+
.sort((left, right) => (
|
|
59
|
+
left.priority - right.priority
|
|
60
|
+
|| left.stage.localeCompare(right.stage)
|
|
61
|
+
|| left.kind.localeCompare(right.kind)
|
|
62
|
+
|| left.path.localeCompare(right.path)
|
|
63
|
+
))
|
|
64
|
+
.slice(0, MAX_MANIFEST_ROWS);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function writeContextManifest(path, rows) {
|
|
68
|
+
const text = stableRows(rows).map((item) => JSON.stringify(item)).join('\n');
|
|
69
|
+
await mkdir(dirname(path), { recursive: true });
|
|
70
|
+
await writeFile(path, `${text}\n`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function readContextManifest(path, options = {}) {
|
|
74
|
+
if (!existsSync(path)) {
|
|
75
|
+
return { status: 'fallback', rows: [], error: null };
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
const text = await readFile(path, 'utf8');
|
|
79
|
+
const cwd = resolve(options.cwd || process.cwd());
|
|
80
|
+
const rows = text.trim()
|
|
81
|
+
? text.trim().split('\n').map((line) => JSON.parse(line))
|
|
82
|
+
: [];
|
|
83
|
+
if (rows.length === 0) {
|
|
84
|
+
return { status: 'invalid', rows: [], error: 'empty_manifest' };
|
|
85
|
+
}
|
|
86
|
+
const valid = rows.every((item) => (
|
|
87
|
+
item
|
|
88
|
+
&& item.schema_version === CONTEXT_MANIFEST_SCHEMA_VERSION
|
|
89
|
+
&& typeof item.path === 'string'
|
|
90
|
+
&& item.path.length > 0
|
|
91
|
+
&& typeof item.kind === 'string'
|
|
92
|
+
&& item.kind.length > 0
|
|
93
|
+
&& typeof item.stage === 'string'
|
|
94
|
+
&& item.stage.length > 0
|
|
95
|
+
&& typeof item.reason === 'string'
|
|
96
|
+
&& item.reason.length > 0
|
|
97
|
+
&& typeof item.priority === 'number'
|
|
98
|
+
&& Number.isFinite(item.priority)
|
|
99
|
+
&& typeof item.required === 'boolean'
|
|
100
|
+
&& typeof item.exists === 'boolean'
|
|
101
|
+
));
|
|
102
|
+
if (!valid) {
|
|
103
|
+
return { status: 'invalid', rows: [], error: 'invalid_manifest_row' };
|
|
104
|
+
}
|
|
105
|
+
const missingRequired = rows.find((item) => item.required && (!item.exists || !existsSync(resolve(cwd, item.path))));
|
|
106
|
+
if (missingRequired) {
|
|
107
|
+
return { status: 'invalid', rows, error: `missing_required_context:${missingRequired.kind}` };
|
|
108
|
+
}
|
|
109
|
+
return { status: 'hit', rows, error: null };
|
|
110
|
+
} catch (error) {
|
|
111
|
+
return { status: 'invalid', rows: [], error: error instanceof Error ? error.message : String(error) };
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function buildContextManifestPath(root) {
|
|
116
|
+
return join(root, 'build-context.jsonl');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function reviewContextManifestPath(root) {
|
|
120
|
+
return join(root, 'review-context.jsonl');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export async function generateBuildContextManifest({ cwd, root, state, slug }) {
|
|
124
|
+
const contextPaths = resolveWorkspaceContextPaths(cwd);
|
|
125
|
+
const contextSetup = await inspectWorkspaceContext(cwd);
|
|
126
|
+
const reviewReworkPath = state.review_rework_artifact_path || join(root, 'review-report.md');
|
|
127
|
+
const requiresReviewRework = state.last_confirmed_transition === 'review->build'
|
|
128
|
+
|| (state.current_stage === 'review' && state.rollback_target === 'build');
|
|
129
|
+
const rows = [
|
|
130
|
+
row(cwd, { stage: 'build', kind: 'spec', path: join(root, 'spec.md'), reason: 'clarified_requirements', priority: 10 }),
|
|
131
|
+
row(cwd, { stage: 'build', kind: 'plan', path: join(root, 'plan.md'), reason: 'implementation_strategy', priority: 20 }),
|
|
132
|
+
row(cwd, { stage: 'build', kind: 'architecture', path: join(root, 'architecture.md'), reason: 'architecture_constraints', priority: 21 }),
|
|
133
|
+
row(cwd, { stage: 'build', kind: 'development-plan', path: join(root, 'development-plan.md'), reason: 'execution_steps', priority: 22 }),
|
|
134
|
+
row(cwd, { stage: 'build', kind: 'test-plan', path: join(root, 'test-plan.md'), reason: 'verification_strategy', priority: 23 }),
|
|
135
|
+
row(cwd, { stage: 'build', kind: 'prd', path: state.plan_artifact_path || join(cwd, '.loopx', 'plans', `prd-${slug}.md`), reason: 'requirements', priority: 30 }),
|
|
136
|
+
row(cwd, { stage: 'build', kind: 'test-spec', path: state.test_spec_artifact_path || join(cwd, '.loopx', 'plans', `test-spec-${slug}.md`), reason: 'test_requirements', priority: 31 }),
|
|
137
|
+
row(cwd, { stage: 'build', kind: 'vertical-slices', path: state.change_artifact_paths?.slices || join(cwd, '.loopx', 'changes', 'active', state.change_id || `chg-${slug}`, 'slices.json'), reason: 'end_to_end_delivery_slices', priority: 32 }),
|
|
138
|
+
row(cwd, { stage: 'build', kind: 'review-rework', path: reviewReworkPath, reason: 'review_requested_implementation_fixes', priority: 33, required: requiresReviewRework }),
|
|
139
|
+
row(cwd, { stage: 'build', kind: 'domain-context', path: contextPaths.domainGlossary, reason: 'domain_vocabulary', priority: 34, required: contextSetup.status !== 'missing' }),
|
|
140
|
+
row(cwd, { stage: 'build', kind: 'agent-domain', path: contextPaths.agentDomain, reason: 'agent_context_rules', priority: 35, required: false }),
|
|
141
|
+
];
|
|
142
|
+
const manifestPath = buildContextManifestPath(root);
|
|
143
|
+
await writeContextManifest(manifestPath, rows);
|
|
144
|
+
return { path: manifestPath, rows: stableRows(rows) };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export async function generateReviewContextManifest({ cwd, root, state, slug }) {
|
|
148
|
+
const contextPaths = resolveWorkspaceContextPaths(cwd);
|
|
149
|
+
const contextSetup = await inspectWorkspaceContext(cwd);
|
|
150
|
+
const rows = [
|
|
151
|
+
row(cwd, { stage: 'review', kind: 'execution-record', path: join(root, 'execution-record.md'), reason: 'execution_evidence', priority: 10 }),
|
|
152
|
+
row(cwd, { stage: 'review', kind: 'test-spec', path: state.test_spec_artifact_path || join(cwd, '.loopx', 'plans', `test-spec-${slug}.md`), reason: 'acceptance_tests', priority: 20 }),
|
|
153
|
+
row(cwd, { stage: 'review', kind: 'prd', path: state.plan_artifact_path || join(cwd, '.loopx', 'plans', `prd-${slug}.md`), reason: 'requirements', priority: 21 }),
|
|
154
|
+
row(cwd, { stage: 'review', kind: 'vertical-slices', path: state.change_artifact_paths?.slices || join(cwd, '.loopx', 'changes', 'active', state.change_id || `chg-${slug}`, 'slices.json'), reason: 'slice_verification_contract', priority: 22 }),
|
|
155
|
+
row(cwd, { stage: 'review', kind: 'domain-context', path: contextPaths.domainGlossary, reason: 'terminology_and_boundary_review', priority: 23, required: contextSetup.status !== 'missing' }),
|
|
156
|
+
row(cwd, { stage: 'review', kind: 'changed-files', path: join(root, 'review-support', 'changed-files.json'), reason: 'changed_file_evidence', priority: 25, required: false }),
|
|
157
|
+
row(cwd, { stage: 'review', kind: 'residual-risks', path: join(root, 'execution-record.md'), reason: 'residual_risk_reference', priority: 26, required: false }),
|
|
158
|
+
row(cwd, { stage: 'review', kind: 'build-support', path: join(root, 'build-support'), reason: 'build_gate_evidence', priority: 30, required: false }),
|
|
159
|
+
row(cwd, { stage: 'review', kind: 'agent-domain', path: contextPaths.agentDomain, reason: 'agent_context_rules', priority: 31, required: false }),
|
|
160
|
+
row(cwd, { stage: 'review', kind: 'state', path: join(root, 'state.json'), reason: 'workflow_state', priority: 40 }),
|
|
161
|
+
];
|
|
162
|
+
const manifestPath = reviewContextManifestPath(root);
|
|
163
|
+
await writeContextManifest(manifestPath, rows);
|
|
164
|
+
return { path: manifestPath, rows: stableRows(rows) };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function manifestRowsToInputManifest(rows, fallback = []) {
|
|
168
|
+
if (!Array.isArray(rows) || rows.length === 0) {
|
|
169
|
+
return fallback;
|
|
170
|
+
}
|
|
171
|
+
return rows.map((item) => `${item.kind}:${item.path}:${item.reason}`);
|
|
172
|
+
}
|