@ai-content-space/loopx 0.1.2 → 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 +18 -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 +47 -5
- 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 +57 -5
- package/src/plan-runtime.mjs +79 -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 +1941 -117
- package/src/workspace-context.mjs +166 -0
- package/src/workspace-memory.mjs +69 -0
|
@@ -1,8 +1,51 @@
|
|
|
1
|
-
import { mkdir, rename } from 'node:fs/promises';
|
|
2
|
-
import { existsSync } from 'node:fs';
|
|
3
|
-
import { join, resolve } from 'node:path';
|
|
1
|
+
import { mkdir, readFile, readdir, rename, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
3
|
+
import { basename, dirname, isAbsolute, join, resolve } from 'node:path';
|
|
4
4
|
|
|
5
|
-
import { inspectInstallState, verifyInstallState } from './install-discovery.mjs';
|
|
5
|
+
import { getTemplateBaselinePath, inspectInstallState, verifyInstallState } from './install-discovery.mjs';
|
|
6
|
+
import { inspectTemplateGovernance } from './template-governance.mjs';
|
|
7
|
+
import { inspectWorkspaceContext } from './workspace-context.mjs';
|
|
8
|
+
|
|
9
|
+
const WORKFLOW_SCHEMA_VERSION = 1;
|
|
10
|
+
|
|
11
|
+
const STAGES = {
|
|
12
|
+
CLARIFY: 'clarify',
|
|
13
|
+
PLAN: 'plan',
|
|
14
|
+
BUILD: 'build',
|
|
15
|
+
REVIEW: 'review',
|
|
16
|
+
DONE: 'done',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const APPROVAL_STATES = {
|
|
20
|
+
NOT_REQUESTED: 'not-requested',
|
|
21
|
+
REQUESTED: 'requested',
|
|
22
|
+
APPROVED: 'approved',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const TRANSITIONS = {
|
|
26
|
+
NONE: 'none',
|
|
27
|
+
CLARIFY_TO_PLAN: 'clarify->plan',
|
|
28
|
+
PLAN_TO_BUILD: 'plan->build',
|
|
29
|
+
BUILD_TO_REVIEW: 'build->review',
|
|
30
|
+
REVIEW_TO_DONE: 'review->done',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const CHANGE_ARTIFACT_FILE_MAP = {
|
|
34
|
+
proposal: 'proposal.md',
|
|
35
|
+
specDelta: 'spec-delta.md',
|
|
36
|
+
design: 'design.md',
|
|
37
|
+
tasks: 'tasks.md',
|
|
38
|
+
slices: 'slices.json',
|
|
39
|
+
graph: 'artifact-graph.json',
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
function normalizeSlug(raw) {
|
|
43
|
+
return String(raw || '')
|
|
44
|
+
.trim()
|
|
45
|
+
.toLowerCase()
|
|
46
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
47
|
+
.replace(/^-+|-+$/g, '');
|
|
48
|
+
}
|
|
6
49
|
|
|
7
50
|
export function resolveLoopxRoot(cwd) {
|
|
8
51
|
return join(resolve(cwd), '.loopx');
|
|
@@ -16,33 +59,383 @@ export function resolveLegacyRoot(cwd) {
|
|
|
16
59
|
return join(resolve(cwd), '.codex-helper');
|
|
17
60
|
}
|
|
18
61
|
|
|
62
|
+
function existsExactPath(path) {
|
|
63
|
+
const parent = dirname(path);
|
|
64
|
+
const name = basename(path);
|
|
65
|
+
if (!existsSync(parent)) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
return readdirSync(parent).includes(name);
|
|
70
|
+
} catch {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
19
75
|
export async function ensureLoopxRoot(cwd) {
|
|
20
76
|
const root = resolveLoopxRoot(cwd);
|
|
21
77
|
const uppercaseRoot = resolveUppercaseLoopxRoot(cwd);
|
|
22
|
-
if (!
|
|
78
|
+
if (!existsExactPath(root) && existsExactPath(uppercaseRoot)) {
|
|
23
79
|
await rename(uppercaseRoot, root);
|
|
24
80
|
}
|
|
25
81
|
await mkdir(root, { recursive: true });
|
|
26
82
|
return root;
|
|
27
83
|
}
|
|
28
84
|
|
|
85
|
+
async function readJsonIfExists(path) {
|
|
86
|
+
if (!existsSync(path)) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
return JSON.parse(await readFile(path, 'utf8'));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function readTextIfExists(path) {
|
|
93
|
+
if (!existsSync(path)) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
return readFile(path, 'utf8');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function parseFrontmatter(text) {
|
|
100
|
+
if (!text?.startsWith('---\n')) {
|
|
101
|
+
return {};
|
|
102
|
+
}
|
|
103
|
+
const end = text.indexOf('\n---\n', 4);
|
|
104
|
+
if (end === -1) {
|
|
105
|
+
return {};
|
|
106
|
+
}
|
|
107
|
+
const result = {};
|
|
108
|
+
for (const line of text.slice(4, end).split('\n')) {
|
|
109
|
+
if (!line || /^\s/.test(line)) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
const separator = line.indexOf(':');
|
|
113
|
+
if (separator === -1) {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const key = line.slice(0, separator).trim();
|
|
117
|
+
const rawValue = line.slice(separator + 1).trim();
|
|
118
|
+
if (rawValue === 'true' || rawValue === 'false') {
|
|
119
|
+
result[key] = rawValue === 'true';
|
|
120
|
+
} else {
|
|
121
|
+
result[key] = rawValue;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return result;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function resolveRuntimePath(cwd, rawPath, fallback) {
|
|
128
|
+
if (!rawPath) {
|
|
129
|
+
return fallback;
|
|
130
|
+
}
|
|
131
|
+
if (isAbsolute(rawPath)) {
|
|
132
|
+
return rawPath;
|
|
133
|
+
}
|
|
134
|
+
return resolve(cwd, rawPath);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function artifactPathFromGraph(cwd, graph, key, fallback) {
|
|
138
|
+
const snakeKey = key === 'specDelta' ? 'spec_delta' : key;
|
|
139
|
+
const rawPath = graph?.change_artifacts?.[snakeKey] || graph?.artifacts?.[key]?.path;
|
|
140
|
+
return resolveRuntimePath(cwd, rawPath, fallback);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function createChangeArtifactPaths(cwd, changeRoot, graph = null) {
|
|
144
|
+
return {
|
|
145
|
+
root: changeRoot,
|
|
146
|
+
proposal: artifactPathFromGraph(cwd, graph, 'proposal', join(changeRoot, CHANGE_ARTIFACT_FILE_MAP.proposal)),
|
|
147
|
+
specDelta: artifactPathFromGraph(cwd, graph, 'specDelta', join(changeRoot, CHANGE_ARTIFACT_FILE_MAP.specDelta)),
|
|
148
|
+
design: artifactPathFromGraph(cwd, graph, 'design', join(changeRoot, CHANGE_ARTIFACT_FILE_MAP.design)),
|
|
149
|
+
tasks: artifactPathFromGraph(cwd, graph, 'tasks', join(changeRoot, CHANGE_ARTIFACT_FILE_MAP.tasks)),
|
|
150
|
+
graph: artifactPathFromGraph(cwd, graph, 'graph', join(changeRoot, CHANGE_ARTIFACT_FILE_MAP.graph)),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function findActiveChangeForWorkflow(cwd, slug) {
|
|
155
|
+
const normalized = normalizeSlug(slug);
|
|
156
|
+
const activeRoot = join(resolveLoopxRoot(cwd), 'changes', 'active');
|
|
157
|
+
if (!existsSync(activeRoot)) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
const entries = await readdir(activeRoot, { withFileTypes: true });
|
|
161
|
+
const candidates = [];
|
|
162
|
+
for (const entry of entries) {
|
|
163
|
+
if (!entry.isDirectory()) {
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
const changeRoot = join(activeRoot, entry.name);
|
|
167
|
+
const graphPath = join(changeRoot, CHANGE_ARTIFACT_FILE_MAP.graph);
|
|
168
|
+
const graph = await readJsonIfExists(graphPath);
|
|
169
|
+
const specDeltaPath = join(changeRoot, CHANGE_ARTIFACT_FILE_MAP.specDelta);
|
|
170
|
+
const specDeltaText = await readTextIfExists(specDeltaPath);
|
|
171
|
+
const specDeltaMeta = parseFrontmatter(specDeltaText);
|
|
172
|
+
let score = 0;
|
|
173
|
+
if (normalizeSlug(graph?.slug || graph?.workflow) === normalized) {
|
|
174
|
+
score += 100;
|
|
175
|
+
}
|
|
176
|
+
if (normalizeSlug(specDeltaMeta.slug) === normalized) {
|
|
177
|
+
score += 80;
|
|
178
|
+
}
|
|
179
|
+
if (normalizeSlug(entry.name).startsWith(`${normalized}-`) || normalizeSlug(entry.name) === normalized) {
|
|
180
|
+
score += 20;
|
|
181
|
+
}
|
|
182
|
+
if (score === 0) {
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
const paths = createChangeArtifactPaths(cwd, changeRoot, graph);
|
|
186
|
+
const changeId = normalizeSlug(graph?.change_id || graph?.change || specDeltaMeta.change_id || entry.name);
|
|
187
|
+
candidates.push({ score, changeId, paths, rootName: entry.name });
|
|
188
|
+
}
|
|
189
|
+
candidates.sort((left, right) => right.score - left.score || right.rootName.localeCompare(left.rootName));
|
|
190
|
+
return candidates[0] || null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function inferReviewState(workflowRoot) {
|
|
194
|
+
const reviewText = await readTextIfExists(join(workflowRoot, 'review.md'))
|
|
195
|
+
|| await readTextIfExists(join(workflowRoot, 'review-report.md'))
|
|
196
|
+
|| '';
|
|
197
|
+
const reviewMeta = parseFrontmatter(reviewText);
|
|
198
|
+
const rawVerdict = String(reviewMeta.verdict || '').toLowerCase();
|
|
199
|
+
const textVerdict = /(^|\n)\s*(REQUEST\s+CHANGES|NO-?GO)\s*($|\n)/i.test(reviewText)
|
|
200
|
+
? 'request-changes'
|
|
201
|
+
: /(^|\n)\s*(APPROVE|GO)\s*($|\n)/i.test(reviewText)
|
|
202
|
+
? 'approve'
|
|
203
|
+
: 'none';
|
|
204
|
+
const reviewVerdict = rawVerdict === 'go' || rawVerdict.includes('approve')
|
|
205
|
+
? 'approve'
|
|
206
|
+
: rawVerdict.includes('request') || rawVerdict === 'no-go' || rawVerdict === 'nogo'
|
|
207
|
+
? 'request-changes'
|
|
208
|
+
: textVerdict;
|
|
209
|
+
if (reviewVerdict === 'approve') {
|
|
210
|
+
return {
|
|
211
|
+
current_stage: STAGES.REVIEW,
|
|
212
|
+
stage_status: 'awaiting-approval',
|
|
213
|
+
review_status: 'in-review',
|
|
214
|
+
review_verdict: 'approve',
|
|
215
|
+
rollback_target: 'none',
|
|
216
|
+
rollback_rationale: null,
|
|
217
|
+
pending_user_decision: TRANSITIONS.REVIEW_TO_DONE,
|
|
218
|
+
requested_transition: TRANSITIONS.NONE,
|
|
219
|
+
last_confirmed_transition: TRANSITIONS.BUILD_TO_REVIEW,
|
|
220
|
+
approval: {
|
|
221
|
+
plan: APPROVAL_STATES.APPROVED,
|
|
222
|
+
build: APPROVAL_STATES.APPROVED,
|
|
223
|
+
review: APPROVAL_STATES.APPROVED,
|
|
224
|
+
rollback: APPROVAL_STATES.NOT_REQUESTED,
|
|
225
|
+
complete: APPROVAL_STATES.REQUESTED,
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function inferExecutionStatus(workflowRoot) {
|
|
233
|
+
const text = await readTextIfExists(join(workflowRoot, 'execution-record.md'));
|
|
234
|
+
if (!text) {
|
|
235
|
+
return 'missing';
|
|
236
|
+
}
|
|
237
|
+
const meta = parseFrontmatter(text);
|
|
238
|
+
if (meta.execution_approved_for_review === true || meta.status === 'review-ready' || /## Verification Evidence/i.test(text)) {
|
|
239
|
+
return 'complete';
|
|
240
|
+
}
|
|
241
|
+
return 'partial';
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function createMigratedWorkflowBaseState(slug, legacyState, change) {
|
|
245
|
+
const profile = legacyState.clarify_profile || legacyState.profile || 'standard';
|
|
246
|
+
return {
|
|
247
|
+
schema_version: WORKFLOW_SCHEMA_VERSION,
|
|
248
|
+
slug,
|
|
249
|
+
current_stage: STAGES.CLARIFY,
|
|
250
|
+
stage_status: 'blocked',
|
|
251
|
+
clarify_profile: profile,
|
|
252
|
+
clarify_target_ambiguity_threshold: legacyState.clarify_target_ambiguity_threshold ?? 0.2,
|
|
253
|
+
clarify_max_rounds: legacyState.clarify_max_rounds ?? 15,
|
|
254
|
+
clarify_current_round: legacyState.clarify_current_round ?? legacyState.current_round ?? 0,
|
|
255
|
+
clarify_ambiguity_score: legacyState.clarify_ambiguity_score ?? legacyState.ambiguity_score ?? 1,
|
|
256
|
+
clarify_pressure_pass_complete: Boolean(legacyState.clarify_pressure_pass_complete ?? legacyState.pressure_pass_complete),
|
|
257
|
+
clarify_non_goals_resolved: Boolean(legacyState.clarify_non_goals_resolved ?? legacyState.non_goals_resolved),
|
|
258
|
+
clarify_decision_boundaries_resolved: Boolean(legacyState.clarify_decision_boundaries_resolved ?? legacyState.decision_boundaries_resolved),
|
|
259
|
+
ambiguity_items: Array.isArray(legacyState.ambiguity_items) ? legacyState.ambiguity_items : [],
|
|
260
|
+
unresolved_ambiguity_count: Number(legacyState.unresolved_ambiguity_count ?? 0),
|
|
261
|
+
plan_package_status: 'missing',
|
|
262
|
+
plan_current_iteration: 0,
|
|
263
|
+
plan_max_iterations: 3,
|
|
264
|
+
plan_consensus_mode: true,
|
|
265
|
+
plan_deliberate_mode: false,
|
|
266
|
+
plan_interactive_mode: false,
|
|
267
|
+
plan_principles_resolved: false,
|
|
268
|
+
plan_options_reviewed: false,
|
|
269
|
+
plan_architect_review_status: 'not-started',
|
|
270
|
+
plan_critic_verdict: 'none',
|
|
271
|
+
plan_acceptance_criteria_testable: false,
|
|
272
|
+
plan_verification_steps_resolved: false,
|
|
273
|
+
plan_execution_inputs_resolved: false,
|
|
274
|
+
plan_docs_status: 'missing',
|
|
275
|
+
plan_docs_artifact_paths: null,
|
|
276
|
+
plan_review_artifact_paths: [],
|
|
277
|
+
plan_blockers: [],
|
|
278
|
+
plan_source_spec_path: null,
|
|
279
|
+
change_id: change?.changeId || `chg-${slug}`,
|
|
280
|
+
change_artifacts_status: change ? 'complete' : 'missing',
|
|
281
|
+
change_artifact_paths: change?.paths || null,
|
|
282
|
+
spec_delta_status: change ? 'complete' : 'missing',
|
|
283
|
+
spec_sync_status: 'pending',
|
|
284
|
+
archive_status: 'pending',
|
|
285
|
+
archived_change_path: null,
|
|
286
|
+
archived_spec_paths: [],
|
|
287
|
+
build_run_id: null,
|
|
288
|
+
build_current_iteration: 0,
|
|
289
|
+
build_max_iterations: 5,
|
|
290
|
+
build_parallel_mode: false,
|
|
291
|
+
build_lane_statuses: [],
|
|
292
|
+
build_verification_status: 'pending',
|
|
293
|
+
build_architect_verification_status: 'not-started',
|
|
294
|
+
build_deslop_status: 'pending',
|
|
295
|
+
build_regression_status: 'pending',
|
|
296
|
+
build_blockers: [],
|
|
297
|
+
build_progress_artifact_paths: [],
|
|
298
|
+
build_support_evidence_paths: [],
|
|
299
|
+
build_no_deslop: false,
|
|
300
|
+
autopilot_current_phase: 'none',
|
|
301
|
+
autopilot_phase_history: [],
|
|
302
|
+
autopilot_blockers: [],
|
|
303
|
+
autopilot_run_path: null,
|
|
304
|
+
autopilot_completed: false,
|
|
305
|
+
review_status: 'not-started',
|
|
306
|
+
rollback_target: 'none',
|
|
307
|
+
rollback_rationale: null,
|
|
308
|
+
pending_user_decision: TRANSITIONS.NONE,
|
|
309
|
+
requested_transition: TRANSITIONS.NONE,
|
|
310
|
+
last_confirmed_transition: TRANSITIONS.NONE,
|
|
311
|
+
approval: {
|
|
312
|
+
plan: APPROVAL_STATES.NOT_REQUESTED,
|
|
313
|
+
build: APPROVAL_STATES.NOT_REQUESTED,
|
|
314
|
+
review: APPROVAL_STATES.NOT_REQUESTED,
|
|
315
|
+
rollback: APPROVAL_STATES.NOT_REQUESTED,
|
|
316
|
+
complete: APPROVAL_STATES.NOT_REQUESTED,
|
|
317
|
+
},
|
|
318
|
+
execution_record_status: 'missing',
|
|
319
|
+
review_verdict: 'none',
|
|
320
|
+
completion_confirmed: false,
|
|
321
|
+
active_run_id: null,
|
|
322
|
+
spec_artifact_path: null,
|
|
323
|
+
plan_artifact_path: null,
|
|
324
|
+
test_spec_artifact_path: null,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async function migrateLegacyWorkflowState(cwd, slug, workflowRoot, legacyState) {
|
|
329
|
+
const change = await findActiveChangeForWorkflow(cwd, slug);
|
|
330
|
+
const reviewState = await inferReviewState(workflowRoot);
|
|
331
|
+
const canonicalPlanPath = join(resolveLoopxRoot(cwd), 'plans', `prd-${slug}.md`);
|
|
332
|
+
const canonicalTestSpecPath = join(resolveLoopxRoot(cwd), 'plans', `test-spec-${slug}.md`);
|
|
333
|
+
const baseState = createMigratedWorkflowBaseState(slug, legacyState, change);
|
|
334
|
+
const planDocsComplete = ['plan.md', 'architecture.md', 'development-plan.md', 'test-plan.md']
|
|
335
|
+
.every((name) => existsSync(join(workflowRoot, name)));
|
|
336
|
+
const executionRecordStatus = await inferExecutionStatus(workflowRoot);
|
|
337
|
+
const planState = planDocsComplete ? {
|
|
338
|
+
current_stage: STAGES.PLAN,
|
|
339
|
+
stage_status: 'awaiting-approval',
|
|
340
|
+
plan_package_status: 'complete',
|
|
341
|
+
plan_current_iteration: 1,
|
|
342
|
+
plan_principles_resolved: true,
|
|
343
|
+
plan_options_reviewed: true,
|
|
344
|
+
plan_architect_review_status: 'complete',
|
|
345
|
+
plan_critic_verdict: 'approve',
|
|
346
|
+
plan_acceptance_criteria_testable: true,
|
|
347
|
+
plan_verification_steps_resolved: true,
|
|
348
|
+
plan_execution_inputs_resolved: true,
|
|
349
|
+
plan_docs_status: 'complete',
|
|
350
|
+
approval: {
|
|
351
|
+
plan: APPROVAL_STATES.APPROVED,
|
|
352
|
+
build: APPROVAL_STATES.NOT_REQUESTED,
|
|
353
|
+
review: APPROVAL_STATES.NOT_REQUESTED,
|
|
354
|
+
rollback: APPROVAL_STATES.NOT_REQUESTED,
|
|
355
|
+
complete: APPROVAL_STATES.NOT_REQUESTED,
|
|
356
|
+
},
|
|
357
|
+
} : {};
|
|
358
|
+
const buildState = executionRecordStatus === 'complete' ? {
|
|
359
|
+
current_stage: STAGES.BUILD,
|
|
360
|
+
stage_status: 'awaiting-approval',
|
|
361
|
+
build_current_iteration: 1,
|
|
362
|
+
build_parallel_mode: true,
|
|
363
|
+
build_verification_status: 'complete',
|
|
364
|
+
build_architect_verification_status: 'approved',
|
|
365
|
+
build_deslop_status: 'complete',
|
|
366
|
+
build_regression_status: 'passed',
|
|
367
|
+
review_status: 'ready-for-review',
|
|
368
|
+
execution_record_status: 'complete',
|
|
369
|
+
approval: {
|
|
370
|
+
...(planState.approval || baseState.approval),
|
|
371
|
+
build: APPROVAL_STATES.APPROVED,
|
|
372
|
+
review: APPROVAL_STATES.NOT_REQUESTED,
|
|
373
|
+
},
|
|
374
|
+
} : {};
|
|
375
|
+
const migrated = {
|
|
376
|
+
...baseState,
|
|
377
|
+
...legacyState,
|
|
378
|
+
...planState,
|
|
379
|
+
...buildState,
|
|
380
|
+
schema_version: WORKFLOW_SCHEMA_VERSION,
|
|
381
|
+
slug,
|
|
382
|
+
clarify_profile: legacyState.clarify_profile || legacyState.profile || 'standard',
|
|
383
|
+
plan_artifact_path: existsSync(canonicalPlanPath) ? canonicalPlanPath : join(workflowRoot, 'plan.md'),
|
|
384
|
+
test_spec_artifact_path: existsSync(canonicalTestSpecPath) ? canonicalTestSpecPath : join(workflowRoot, 'test-plan.md'),
|
|
385
|
+
execution_record_status: executionRecordStatus,
|
|
386
|
+
...(reviewState || {}),
|
|
387
|
+
};
|
|
388
|
+
await writeFile(join(workflowRoot, 'state.json'), `${JSON.stringify(migrated, null, 2)}\n`);
|
|
389
|
+
return {
|
|
390
|
+
slug,
|
|
391
|
+
migrated: true,
|
|
392
|
+
reason: 'migrated_legacy_workflow_schema',
|
|
393
|
+
current_stage: migrated.current_stage,
|
|
394
|
+
change_id: migrated.change_id,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
async function migrateLegacyWorkflowStates(cwd) {
|
|
399
|
+
const workflowsRoot = join(resolveLoopxRoot(cwd), 'workflows');
|
|
400
|
+
if (!existsSync(workflowsRoot)) {
|
|
401
|
+
return [];
|
|
402
|
+
}
|
|
403
|
+
const entries = await readdir(workflowsRoot, { withFileTypes: true });
|
|
404
|
+
const migrations = [];
|
|
405
|
+
for (const entry of entries) {
|
|
406
|
+
if (!entry.isDirectory()) {
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
const slug = normalizeSlug(entry.name);
|
|
410
|
+
const workflowRoot = join(workflowsRoot, entry.name);
|
|
411
|
+
const state = await readJsonIfExists(join(workflowRoot, 'state.json'));
|
|
412
|
+
if (!state || state.schema_version === WORKFLOW_SCHEMA_VERSION) {
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
migrations.push(await migrateLegacyWorkflowState(cwd, slug, workflowRoot, state));
|
|
416
|
+
}
|
|
417
|
+
return migrations;
|
|
418
|
+
}
|
|
419
|
+
|
|
29
420
|
export async function migrateLegacyRuntime(cwd) {
|
|
30
421
|
const legacyRoot = resolveLegacyRoot(cwd);
|
|
31
422
|
const loopxRoot = resolveLoopxRoot(cwd);
|
|
32
423
|
const uppercaseRoot = resolveUppercaseLoopxRoot(cwd);
|
|
33
|
-
const legacyExists =
|
|
34
|
-
const loopxExists =
|
|
35
|
-
const uppercaseExists =
|
|
424
|
+
const legacyExists = existsExactPath(legacyRoot);
|
|
425
|
+
const loopxExists = existsExactPath(loopxRoot);
|
|
426
|
+
const uppercaseExists = existsExactPath(uppercaseRoot);
|
|
36
427
|
|
|
37
428
|
if (!legacyExists && !uppercaseExists) {
|
|
429
|
+
const workflowStateMigrations = loopxExists ? await migrateLegacyWorkflowStates(cwd) : [];
|
|
38
430
|
return {
|
|
39
|
-
migrated:
|
|
431
|
+
migrated: workflowStateMigrations.length > 0,
|
|
40
432
|
legacyExists: false,
|
|
41
433
|
uppercaseExists: false,
|
|
42
434
|
loopxExists,
|
|
43
435
|
loopxRoot,
|
|
44
436
|
legacyRoot,
|
|
45
|
-
|
|
437
|
+
workflowStateMigrations,
|
|
438
|
+
reason: workflowStateMigrations.length > 0 ? 'migrated_legacy_workflow_schema' : 'legacy_root_missing',
|
|
46
439
|
};
|
|
47
440
|
}
|
|
48
441
|
|
|
@@ -52,6 +445,7 @@ export async function migrateLegacyRuntime(cwd) {
|
|
|
52
445
|
|
|
53
446
|
if (uppercaseExists && !loopxExists) {
|
|
54
447
|
await rename(uppercaseRoot, loopxRoot);
|
|
448
|
+
const workflowStateMigrations = await migrateLegacyWorkflowStates(cwd);
|
|
55
449
|
return {
|
|
56
450
|
migrated: true,
|
|
57
451
|
legacyExists,
|
|
@@ -59,11 +453,13 @@ export async function migrateLegacyRuntime(cwd) {
|
|
|
59
453
|
loopxExists: true,
|
|
60
454
|
loopxRoot,
|
|
61
455
|
legacyRoot,
|
|
456
|
+
workflowStateMigrations,
|
|
62
457
|
reason: 'migrated_uppercase_loopx_runtime',
|
|
63
458
|
};
|
|
64
459
|
}
|
|
65
460
|
|
|
66
461
|
await rename(legacyRoot, loopxRoot);
|
|
462
|
+
const workflowStateMigrations = await migrateLegacyWorkflowStates(cwd);
|
|
67
463
|
return {
|
|
68
464
|
migrated: true,
|
|
69
465
|
legacyExists: true,
|
|
@@ -71,6 +467,7 @@ export async function migrateLegacyRuntime(cwd) {
|
|
|
71
467
|
loopxExists: true,
|
|
72
468
|
loopxRoot,
|
|
73
469
|
legacyRoot,
|
|
470
|
+
workflowStateMigrations,
|
|
74
471
|
reason: 'migrated_legacy_runtime',
|
|
75
472
|
};
|
|
76
473
|
}
|
|
@@ -81,16 +478,33 @@ export async function doctorRuntime(cwd, env = process.env) {
|
|
|
81
478
|
const uppercaseRoot = resolveUppercaseLoopxRoot(cwd);
|
|
82
479
|
const installState = await inspectInstallState(env);
|
|
83
480
|
const installCheck = await verifyInstallState(env);
|
|
481
|
+
const installTemplateBaselinePath = getTemplateBaselinePath(env);
|
|
482
|
+
const workspaceTemplateBaselinePath = join(loopxRoot, 'template-hashes.json');
|
|
483
|
+
const templateGovernance = await inspectTemplateGovernance(
|
|
484
|
+
existsSync(installTemplateBaselinePath) ? installTemplateBaselinePath : workspaceTemplateBaselinePath,
|
|
485
|
+
);
|
|
486
|
+
const workflowHookPath = join(resolve(cwd), 'scripts', 'codex-workflow-hook.mjs');
|
|
487
|
+
const installedWorkflowHookPath = installState.managedArtifacts?.['codex-workflow-hook']?.targetPath
|
|
488
|
+
|| join(resolve(env.LOOPX_HOME || env.HOME || process.cwd()), '.codex', 'hooks', 'codex-workflow-hook.mjs');
|
|
489
|
+
const hook = {
|
|
490
|
+
enabled: env.LOOPX_HOOKS !== '0',
|
|
491
|
+
workflowHookPath,
|
|
492
|
+
installedWorkflowHookPath,
|
|
493
|
+
installed: existsSync(installedWorkflowHookPath),
|
|
494
|
+
};
|
|
84
495
|
|
|
85
496
|
return {
|
|
86
497
|
loopxRoot,
|
|
87
498
|
legacyRoot,
|
|
88
499
|
uppercaseRoot,
|
|
89
|
-
loopxExists:
|
|
90
|
-
legacyExists:
|
|
91
|
-
uppercaseExists:
|
|
92
|
-
mixedRuntimeRoots:
|
|
500
|
+
loopxExists: existsExactPath(loopxRoot),
|
|
501
|
+
legacyExists: existsExactPath(legacyRoot),
|
|
502
|
+
uppercaseExists: existsExactPath(uppercaseRoot),
|
|
503
|
+
mixedRuntimeRoots: existsExactPath(loopxRoot) && (existsExactPath(legacyRoot) || existsExactPath(uppercaseRoot)),
|
|
93
504
|
installState,
|
|
94
505
|
installCheck,
|
|
506
|
+
templateGovernance,
|
|
507
|
+
contextSetup: await inspectWorkspaceContext(cwd),
|
|
508
|
+
hook,
|
|
95
509
|
};
|
|
96
510
|
}
|