@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.
Files changed (67) hide show
  1. package/README.md +343 -56
  2. package/README.zh-CN.md +392 -0
  3. package/package.json +4 -1
  4. package/plugins/loopx/.codex-plugin/plugin.json +1 -1
  5. package/plugins/loopx/scripts/plugin-install.test.mjs +1 -0
  6. package/plugins/loopx/skills/archive/SKILL.md +39 -0
  7. package/plugins/loopx/skills/build/SKILL.md +111 -9
  8. package/plugins/loopx/skills/clarify/SKILL.md +121 -1
  9. package/plugins/loopx/skills/debug/SKILL.md +296 -0
  10. package/plugins/loopx/skills/debug/condition-based-waiting.md +115 -0
  11. package/plugins/loopx/skills/debug/defense-in-depth.md +122 -0
  12. package/plugins/loopx/skills/debug/find-polluter.sh +63 -0
  13. package/plugins/loopx/skills/debug/root-cause-tracing.md +169 -0
  14. package/plugins/loopx/skills/go-style/SKILL.md +71 -0
  15. package/plugins/loopx/skills/kratos/SKILL.md +74 -0
  16. package/plugins/loopx/skills/kratos/references/advanced-features.md +314 -0
  17. package/plugins/loopx/skills/kratos/references/architecture.md +488 -0
  18. package/plugins/loopx/skills/kratos/references/configuration.md +399 -0
  19. package/plugins/loopx/skills/kratos/references/http-customization.md +512 -0
  20. package/plugins/loopx/skills/kratos/references/middleware-logging.md +400 -0
  21. package/plugins/loopx/skills/kratos/references/proto-api-design.md +432 -0
  22. package/plugins/loopx/skills/kratos/references/security-auth.md +411 -0
  23. package/plugins/loopx/skills/kratos/references/troubleshooting.md +385 -0
  24. package/plugins/loopx/skills/plan/SKILL.md +22 -2
  25. package/plugins/loopx/skills/review/SKILL.md +98 -1
  26. package/plugins/loopx/skills/tdd/SKILL.md +371 -0
  27. package/plugins/loopx/skills/tdd/testing-anti-patterns.md +299 -0
  28. package/plugins/loopx/skills/verify/SKILL.md +139 -0
  29. package/scripts/codex-stop-hook.mjs +71 -0
  30. package/scripts/codex-workflow-hook.mjs +153 -0
  31. package/skills/archive/SKILL.md +39 -0
  32. package/skills/build/SKILL.md +111 -9
  33. package/skills/clarify/SKILL.md +121 -1
  34. package/skills/debug/SKILL.md +296 -0
  35. package/skills/debug/condition-based-waiting.md +115 -0
  36. package/skills/debug/defense-in-depth.md +122 -0
  37. package/skills/debug/find-polluter.sh +63 -0
  38. package/skills/debug/root-cause-tracing.md +169 -0
  39. package/skills/go-style/SKILL.md +71 -0
  40. package/skills/kratos/SKILL.md +74 -0
  41. package/skills/kratos/references/advanced-features.md +314 -0
  42. package/skills/kratos/references/architecture.md +488 -0
  43. package/skills/kratos/references/configuration.md +399 -0
  44. package/skills/kratos/references/http-customization.md +512 -0
  45. package/skills/kratos/references/middleware-logging.md +400 -0
  46. package/skills/kratos/references/proto-api-design.md +432 -0
  47. package/skills/kratos/references/security-auth.md +411 -0
  48. package/skills/kratos/references/troubleshooting.md +385 -0
  49. package/skills/plan/SKILL.md +18 -2
  50. package/skills/review/SKILL.md +98 -1
  51. package/skills/tdd/SKILL.md +371 -0
  52. package/skills/tdd/testing-anti-patterns.md +299 -0
  53. package/skills/verify/SKILL.md +139 -0
  54. package/src/build-runtime.mjs +303 -26
  55. package/src/build-stop-gate.mjs +94 -0
  56. package/src/cli.mjs +47 -5
  57. package/src/codex-exec-runtime.mjs +105 -5
  58. package/src/context-manifest.mjs +172 -0
  59. package/src/install-discovery.mjs +352 -5
  60. package/src/next-skill.mjs +57 -5
  61. package/src/plan-runtime.mjs +79 -122
  62. package/src/review-runtime.mjs +378 -0
  63. package/src/runtime-maintenance.mjs +428 -14
  64. package/src/template-governance.mjs +223 -0
  65. package/src/workflow.mjs +1941 -117
  66. package/src/workspace-context.mjs +166 -0
  67. package/src/workspace-memory.mjs +69 -0
@@ -4,6 +4,14 @@ import { createHash } from 'node:crypto';
4
4
  import { dirname, join, resolve } from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
6
6
 
7
+ import {
8
+ classifyTemplateDrift,
9
+ createTemplateBaseline,
10
+ inspectTemplateGovernance,
11
+ readTemplateBaseline,
12
+ writeTemplateBaseline,
13
+ } from './template-governance.mjs';
14
+
7
15
  const MODULE_DIR = dirname(fileURLToPath(import.meta.url));
8
16
  const PROJECT_ROOT = resolve(MODULE_DIR, '..');
9
17
  const LOOPX_SKILLS = [
@@ -12,8 +20,69 @@ const LOOPX_SKILLS = [
12
20
  'build',
13
21
  'review',
14
22
  'autopilot',
23
+ 'archive',
24
+ 'debug',
25
+ 'tdd',
26
+ 'verify',
27
+ 'go-style',
28
+ 'kratos',
15
29
  ];
16
30
  const LOOPX_INSTALLATION_IDENTITY = 'loopx';
31
+ const LOOPX_MANAGED_SCRIPT_ITEMS = [
32
+ {
33
+ name: 'codex-workflow-hook',
34
+ kind: 'hook',
35
+ sourceRelativePath: 'scripts/codex-workflow-hook.mjs',
36
+ targetRelativePath: '.codex/hooks/codex-workflow-hook.mjs',
37
+ },
38
+ ];
39
+ const LOOPX_GOVERNED_SOURCE_ITEMS = [
40
+ {
41
+ name: 'loopx-plugin-manifest',
42
+ kind: 'plugin',
43
+ sourceRelativePath: 'plugins/loopx/.codex-plugin/plugin.json',
44
+ },
45
+ {
46
+ name: 'loopx-plugin-install-script',
47
+ kind: 'plugin',
48
+ sourceRelativePath: 'plugins/loopx/scripts/plugin-install.mjs',
49
+ },
50
+ {
51
+ name: 'workflow-template-architecture',
52
+ kind: 'workflow-template',
53
+ sourceRelativePath: 'templates/architecture.md',
54
+ },
55
+ {
56
+ name: 'workflow-template-development-plan',
57
+ kind: 'workflow-template',
58
+ sourceRelativePath: 'templates/development-plan.md',
59
+ },
60
+ {
61
+ name: 'workflow-template-execution-record',
62
+ kind: 'workflow-template',
63
+ sourceRelativePath: 'templates/execution-record.md',
64
+ },
65
+ {
66
+ name: 'workflow-template-plan',
67
+ kind: 'workflow-template',
68
+ sourceRelativePath: 'templates/plan.md',
69
+ },
70
+ {
71
+ name: 'workflow-template-review-report',
72
+ kind: 'workflow-template',
73
+ sourceRelativePath: 'templates/review-report.md',
74
+ },
75
+ {
76
+ name: 'workflow-template-spec',
77
+ kind: 'workflow-template',
78
+ sourceRelativePath: 'templates/spec.md',
79
+ },
80
+ {
81
+ name: 'workflow-template-test-plan',
82
+ kind: 'workflow-template',
83
+ sourceRelativePath: 'templates/test-plan.md',
84
+ },
85
+ ];
17
86
 
18
87
  function jsonClone(value) {
19
88
  return JSON.parse(JSON.stringify(value));
@@ -44,12 +113,18 @@ export function getSkillSourceRoot(env = process.env) {
44
113
  return resolve(env.LOOPX_SKILL_SOURCE_ROOT || join(getProjectRoot(env), 'skills'));
45
114
  }
46
115
 
116
+ export function getTemplateBaselinePath(env = process.env) {
117
+ const home = resolve(env.LOOPX_HOME || env.HOME || process.cwd());
118
+ return resolve(env.LOOPX_TEMPLATE_BASELINE_PATH || join(home, '.loopx', 'template-hashes.json'));
119
+ }
120
+
47
121
  function getInstallOptions(options = {}, env = process.env) {
48
122
  return {
49
123
  installationIdentity: options.installationIdentity || env.LOOPX_INSTALLATION_IDENTITY || LOOPX_INSTALLATION_IDENTITY,
50
124
  distributionChannel: options.distributionChannel || env.LOOPX_DISTRIBUTION_CHANNEL || 'npm',
51
125
  sourceUrl: resolve(options.sourceUrl || env.LOOPX_SOURCE_URL || getProjectRoot(env)),
52
126
  skillSourceRoot: resolve(options.skillSourceRoot || getSkillSourceRoot(env)),
127
+ installMethod: options.installMethod || env.LOOPX_INSTALL_METHOD || 'copy',
53
128
  };
54
129
  }
55
130
 
@@ -57,6 +132,14 @@ function skillSourceDir(skillName, env = process.env, skillSourceRoot = getSkill
57
132
  return join(skillSourceRoot, skillName);
58
133
  }
59
134
 
135
+ function projectSourceEntry(relativePath, env = process.env) {
136
+ const configuredPath = join(getProjectRoot(env), relativePath);
137
+ if (existsSync(configuredPath)) {
138
+ return configuredPath;
139
+ }
140
+ return join(PROJECT_ROOT, relativePath);
141
+ }
142
+
60
143
  function skillSourceEntry(skillName, env = process.env, skillSourceRoot = getSkillSourceRoot(env)) {
61
144
  return join(skillSourceDir(skillName, env, skillSourceRoot), 'SKILL.md');
62
145
  }
@@ -65,6 +148,10 @@ function installedSkillDir(skillName, env = process.env) {
65
148
  return join(getInstalledSkillsRoot(env), skillName);
66
149
  }
67
150
 
151
+ function installedManagedScriptPath(item, env = process.env) {
152
+ return join(installTemplateRoot(env), item.targetRelativePath);
153
+ }
154
+
68
155
  async function fileHash(path) {
69
156
  const hash = createHash('sha1');
70
157
  const stat = await lstat(path);
@@ -131,11 +218,15 @@ async function materializeSkill(skillName, env = process.env, options = {}) {
131
218
  await ensureDir(dirname(targetDir));
132
219
  await removeInstalledSkill(targetDir);
133
220
 
134
- let installMethod = 'symlink';
135
- try {
136
- await symlink(sourceDir, targetDir, 'dir');
137
- } catch {
138
- installMethod = 'copy';
221
+ let installMethod = options.installMethod === 'symlink' ? 'symlink' : 'copy';
222
+ if (installMethod === 'symlink') {
223
+ try {
224
+ await symlink(sourceDir, targetDir, 'dir');
225
+ } catch {
226
+ installMethod = 'copy';
227
+ await cp(sourceDir, targetDir, { recursive: true });
228
+ }
229
+ } else {
139
230
  await cp(sourceDir, targetDir, { recursive: true });
140
231
  }
141
232
 
@@ -148,6 +239,119 @@ async function materializeSkill(skillName, env = process.env, options = {}) {
148
239
  };
149
240
  }
150
241
 
242
+ function installTemplateRoot(env = process.env) {
243
+ return resolve(env.LOOPX_HOME || env.HOME || process.cwd());
244
+ }
245
+
246
+ function templateItemKey(item) {
247
+ return `${item.kind || 'file'}:${item.path}`;
248
+ }
249
+
250
+ function skillTemplatePaths(skillName, env = process.env, options = {}) {
251
+ return {
252
+ targetPath: skillSourceEntry(skillName, env, getInstalledSkillsRoot(env)),
253
+ sourcePath: skillSourceEntry(skillName, env, options.skillSourceRoot),
254
+ };
255
+ }
256
+
257
+ function managedScriptTemplatePaths(item, env = process.env) {
258
+ return {
259
+ targetPath: installedManagedScriptPath(item, env),
260
+ sourcePath: projectSourceEntry(item.sourceRelativePath, env),
261
+ };
262
+ }
263
+
264
+ function governedSourceTemplatePaths(item, env = process.env) {
265
+ const sourcePath = projectSourceEntry(item.sourceRelativePath, env);
266
+ return {
267
+ targetPath: sourcePath,
268
+ sourcePath,
269
+ };
270
+ }
271
+
272
+ async function createSkillTemplateItem(skillName, env = process.env, options = {}) {
273
+ const { targetPath, sourcePath } = skillTemplatePaths(skillName, env, options);
274
+ const baseline = await createTemplateBaseline(installTemplateRoot(env), [{
275
+ path: targetPath,
276
+ sourcePath,
277
+ kind: 'skill',
278
+ }], {
279
+ registryRevision: options.sourceUrl || 'local',
280
+ });
281
+ return baseline.items[0];
282
+ }
283
+
284
+ async function createManagedScriptTemplateItem(item, env = process.env) {
285
+ const { targetPath, sourcePath } = managedScriptTemplatePaths(item, env);
286
+ if (!existsSync(sourcePath)) {
287
+ return null;
288
+ }
289
+ const baseline = await createTemplateBaseline(installTemplateRoot(env), [{
290
+ path: targetPath,
291
+ sourcePath,
292
+ kind: item.kind || 'file',
293
+ }], {
294
+ registryRevision: item.sourceRelativePath,
295
+ });
296
+ return baseline.items[0];
297
+ }
298
+
299
+ async function createGovernedSourceTemplateItem(item, env = process.env, options = {}) {
300
+ const { targetPath, sourcePath } = governedSourceTemplatePaths(item, env);
301
+ if (!existsSync(sourcePath)) {
302
+ return null;
303
+ }
304
+ const baseline = await createTemplateBaseline(installTemplateRoot(env), [{
305
+ path: targetPath,
306
+ sourcePath,
307
+ kind: item.kind || 'file',
308
+ }], {
309
+ registryRevision: options.sourceUrl || item.sourceRelativePath,
310
+ });
311
+ return baseline.items[0];
312
+ }
313
+
314
+ async function templateGovernanceBeforeInstall(skillName, baselineItemsByPath, env = process.env, options = {}) {
315
+ const { targetPath, sourcePath } = skillTemplatePaths(skillName, env, options);
316
+ const probe = await createSkillTemplateItem(skillName, env, options);
317
+ const existing = baselineItemsByPath.get(templateItemKey(probe));
318
+ if (!existing) {
319
+ return { action: 'install', drift: { status: 'unknown', reason: 'missing_baseline_item' }, item: null };
320
+ }
321
+ const drift = await classifyTemplateDrift(existing, {
322
+ root: installTemplateRoot(env),
323
+ targetPath,
324
+ sourcePath,
325
+ });
326
+ if (drift.status === 'user-modified' || drift.status === 'conflict') {
327
+ return { action: 'skip-user-modified', drift, item: existing };
328
+ }
329
+ return { action: 'install', drift, item: existing };
330
+ }
331
+
332
+ async function mergedSkippedTemplateItem(skillName, existing, env = process.env, options = {}) {
333
+ const latest = await createSkillTemplateItem(skillName, env, options);
334
+ return {
335
+ ...existing,
336
+ source_path: latest.source_path,
337
+ registry_hash: latest.registry_hash,
338
+ registry_managed_block_hashes: latest.registry_managed_block_hashes,
339
+ };
340
+ }
341
+
342
+ async function mergedSkippedManagedScriptItem(item, existing, env = process.env) {
343
+ const latest = await createManagedScriptTemplateItem(item, env);
344
+ if (!latest) {
345
+ return existing;
346
+ }
347
+ return {
348
+ ...existing,
349
+ source_path: latest.source_path,
350
+ registry_hash: latest.registry_hash,
351
+ registry_managed_block_hashes: latest.registry_managed_block_hashes,
352
+ };
353
+ }
354
+
151
355
  function buildRegistryRow(record, env = process.env, options = {}) {
152
356
  return {
153
357
  source: 'loopx',
@@ -199,6 +403,18 @@ async function removeStaleOwnedInstall(currentRow) {
199
403
  await removeInstalledSkill(currentRow.installedPath);
200
404
  }
201
405
 
406
+ async function removeInstalledFile(path) {
407
+ if (!existsSync(path)) {
408
+ return;
409
+ }
410
+ const stat = await lstat(path);
411
+ if (stat.isSymbolicLink()) {
412
+ await unlink(path);
413
+ return;
414
+ }
415
+ await rm(path, { force: true });
416
+ }
417
+
202
418
  async function canonicalTargetOwnership(skillName, env = process.env, options = {}) {
203
419
  const targetDir = installedSkillDir(skillName, env);
204
420
  const sourceDir = skillSourceDir(skillName, env, options.skillSourceRoot);
@@ -226,6 +442,31 @@ async function canonicalTargetOwnership(skillName, env = process.env, options =
226
442
  return { exists: true, owned: false };
227
443
  }
228
444
 
445
+ async function canonicalFileOwnership(targetPath, sourcePath) {
446
+ if (!existsSync(targetPath)) {
447
+ return { exists: false, owned: false };
448
+ }
449
+
450
+ const stat = await lstat(targetPath);
451
+ if (stat.isSymbolicLink()) {
452
+ const linkTarget = await readlink(targetPath);
453
+ const resolvedLink = resolve(dirname(targetPath), linkTarget);
454
+ return {
455
+ exists: true,
456
+ owned: resolvedLink === sourcePath,
457
+ };
458
+ }
459
+
460
+ if (stat.isFile()) {
461
+ return {
462
+ exists: true,
463
+ owned: await fileHash(targetPath) === await fileHash(sourcePath),
464
+ };
465
+ }
466
+
467
+ return { exists: true, owned: false };
468
+ }
469
+
229
470
  async function assertLoopxOwnedTarget(skillName, currentRow, env = process.env, options = {}) {
230
471
  const targetDir = installedSkillDir(skillName, env);
231
472
  const dirExists = existsSync(targetDir);
@@ -283,11 +524,40 @@ export async function inspectInstallState(env = process.env) {
283
524
  };
284
525
  }
285
526
 
527
+ const managedArtifacts = {};
528
+ for (const item of LOOPX_MANAGED_SCRIPT_ITEMS) {
529
+ const targetPath = installedManagedScriptPath(item, env);
530
+ const sourcePath = projectSourceEntry(item.sourceRelativePath, env);
531
+ if (!existsSync(sourcePath)) {
532
+ managedArtifacts[item.name] = {
533
+ kind: item.kind,
534
+ targetPath,
535
+ sourcePath,
536
+ installed: existsSync(targetPath),
537
+ discovered: false,
538
+ loopxOwned: false,
539
+ available: false,
540
+ };
541
+ continue;
542
+ }
543
+ const ownership = await canonicalFileOwnership(targetPath, sourcePath);
544
+ managedArtifacts[item.name] = {
545
+ kind: item.kind,
546
+ targetPath,
547
+ sourcePath,
548
+ installed: ownership.exists,
549
+ discovered: ownership.exists && ownership.owned,
550
+ loopxOwned: ownership.owned,
551
+ available: true,
552
+ };
553
+ }
554
+
286
555
  return {
287
556
  projectRoot: getProjectRoot(env),
288
557
  installedSkillsRoot: installedRoot,
289
558
  skillLockPath: getSkillLockPath(env),
290
559
  skills: bySkill,
560
+ managedArtifacts,
291
561
  };
292
562
  }
293
563
 
@@ -308,6 +578,19 @@ export async function verifyInstallState(env = process.env) {
308
578
  }
309
579
  }
310
580
 
581
+ for (const item of LOOPX_MANAGED_SCRIPT_ITEMS) {
582
+ const info = inspection.managedArtifacts?.[item.name];
583
+ if (!info?.available) {
584
+ continue;
585
+ }
586
+ if (!info?.installed) {
587
+ failures.push(`missing_managed_artifact:${item.name}`);
588
+ }
589
+ if (!info?.discovered) {
590
+ failures.push(`managed_artifact_unowned:${item.name}`);
591
+ }
592
+ }
593
+
311
594
  return {
312
595
  ok: failures.length === 0,
313
596
  failures,
@@ -321,9 +604,14 @@ export async function installBundledSkills(env = process.env, options = {}) {
321
604
  const nextData = jsonClone(data);
322
605
  nextData.version = nextData.version || 3;
323
606
  nextData.skills = nextData.skills || {};
607
+ const baselinePath = getTemplateBaselinePath(env);
608
+ const existingBaseline = await readTemplateBaseline(baselinePath);
609
+ const baselineItemsByPath = new Map((existingBaseline?.items || []).map((item) => [templateItemKey(item), item]));
324
610
 
325
611
  const installed = [];
326
612
  const conflicts = [];
613
+ const skipped = [];
614
+ const nextTemplateItems = [];
327
615
  for (const skillName of LOOPX_SKILLS) {
328
616
  const current = nextData.skills[skillName];
329
617
  const ownership = await assertLoopxOwnedTarget(skillName, current, env, installOptions);
@@ -335,6 +623,16 @@ export async function installBundledSkills(env = process.env, options = {}) {
335
623
  });
336
624
  continue;
337
625
  }
626
+ const governance = await templateGovernanceBeforeInstall(skillName, baselineItemsByPath, env, installOptions);
627
+ if (governance.action === 'skip-user-modified') {
628
+ skipped.push({
629
+ skillName,
630
+ reason: governance.drift.status,
631
+ installedPath: ownership.targetDir,
632
+ });
633
+ nextTemplateItems.push(await mergedSkippedTemplateItem(skillName, governance.item, env, installOptions));
634
+ continue;
635
+ }
338
636
  const record = await materializeSkill(skillName, env, installOptions);
339
637
  const row = buildRegistryRow(record, env, installOptions);
340
638
  if (current?.installedAt) {
@@ -348,14 +646,63 @@ export async function installBundledSkills(env = process.env, options = {}) {
348
646
  row.provenance = mergedProvenance;
349
647
  }
350
648
  nextData.skills[skillName] = row;
649
+ nextTemplateItems.push(await createSkillTemplateItem(skillName, env, installOptions));
351
650
  installed.push(row);
352
651
  }
353
652
 
653
+ for (const item of LOOPX_MANAGED_SCRIPT_ITEMS) {
654
+ const { targetPath, sourcePath } = managedScriptTemplatePaths(item, env);
655
+ if (!existsSync(sourcePath)) {
656
+ continue;
657
+ }
658
+ const probe = await createManagedScriptTemplateItem(item, env);
659
+ if (!probe) {
660
+ continue;
661
+ }
662
+ const existing = baselineItemsByPath.get(templateItemKey(probe));
663
+ if (existing) {
664
+ const drift = await classifyTemplateDrift(existing, {
665
+ root: installTemplateRoot(env),
666
+ targetPath,
667
+ sourcePath,
668
+ });
669
+ if (drift.status === 'user-modified' || drift.status === 'conflict') {
670
+ skipped.push({
671
+ skillName: item.name,
672
+ reason: drift.status,
673
+ installedPath: targetPath,
674
+ });
675
+ nextTemplateItems.push(await mergedSkippedManagedScriptItem(item, existing, env));
676
+ continue;
677
+ }
678
+ }
679
+ await ensureDir(dirname(targetPath));
680
+ await removeInstalledFile(targetPath);
681
+ await cp(sourcePath, targetPath);
682
+ nextTemplateItems.push(await createManagedScriptTemplateItem(item, env));
683
+ }
684
+
685
+ for (const item of LOOPX_GOVERNED_SOURCE_ITEMS) {
686
+ const templateItem = await createGovernedSourceTemplateItem(item, env, installOptions);
687
+ if (templateItem) {
688
+ nextTemplateItems.push(templateItem);
689
+ }
690
+ }
691
+
354
692
  await writeSkillLock(nextData, env);
693
+ await writeTemplateBaseline(baselinePath, {
694
+ schema_version: 1,
695
+ generated_by: 'loopx',
696
+ registry_revision: installOptions.sourceUrl || 'local',
697
+ items: nextTemplateItems,
698
+ });
699
+ const templateGovernance = await inspectTemplateGovernance(baselinePath);
355
700
  return {
356
701
  ok: conflicts.length === 0,
357
702
  installed,
358
703
  conflicts,
704
+ skipped,
705
+ templateGovernance,
359
706
  inspection: await inspectInstallState(env),
360
707
  };
361
708
  }
@@ -1,15 +1,67 @@
1
1
  export function nextSkillCommand(state) {
2
- if (!state || state.stage_status !== 'awaiting-approval' || !state.slug) {
2
+ if (!state || !state.slug) {
3
3
  return null;
4
4
  }
5
- if (state.current_stage === 'clarify') {
5
+ const reviewBuildCommand = `$build --from-review .loopx/workflows/${state.slug}/review-report.md`;
6
+ if (state.current_stage === 'clarify'
7
+ && state.clarify_current_round > 0
8
+ && state.unresolved_ambiguity_count === 0
9
+ && state.clarify_non_goals_resolved === true
10
+ && state.clarify_decision_boundaries_resolved === true
11
+ && state.clarify_pressure_pass_complete === true
12
+ && typeof state.clarify_ambiguity_score === 'number'
13
+ && typeof state.clarify_target_ambiguity_threshold === 'number'
14
+ && state.clarify_ambiguity_score <= state.clarify_target_ambiguity_threshold) {
6
15
  return `$plan ${state.slug}`;
7
16
  }
17
+ if (state.current_stage === 'done'
18
+ && state.completion_confirmed === true
19
+ && state.archive_status !== 'archived') {
20
+ return `$archive ${state.slug}`;
21
+ }
22
+ if (state.stage_status !== 'awaiting-approval') {
23
+ return null;
24
+ }
8
25
  if (state.current_stage === 'plan' && Array.isArray(state.plan_blockers) && state.plan_blockers.length === 0) {
9
- return `$build ${state.slug}`;
26
+ return `$build .loopx/plans/prd-${state.slug}.md`;
27
+ }
28
+ if (state.current_stage === 'build'
29
+ && state.stage_status === 'awaiting-approval'
30
+ && state.pending_user_decision === 'build->review'
31
+ && state.review_status === 'ready-for-review'
32
+ && state.execution_record_status === 'complete'
33
+ && Array.isArray(state.build_blockers)
34
+ && state.build_blockers.length === 0) {
35
+ return `$review .loopx/workflows/${state.slug}/execution-record.md`;
36
+ }
37
+ if (state.current_stage === 'review'
38
+ && state.review_verdict === 'request-changes'
39
+ && state.rollback_target === 'build'
40
+ && (
41
+ state.pending_user_decision === 'review->build'
42
+ || state.requested_transition === 'review->build'
43
+ || state.approval?.build === 'requested'
44
+ || state.approval?.build === 'approved'
45
+ )) {
46
+ return reviewBuildCommand;
47
+ }
48
+ if (state.current_stage === 'review'
49
+ && state.review_verdict === 'request-changes'
50
+ && state.requested_transition === 'review->build'
51
+ && state.approval?.build === 'approved') {
52
+ return reviewBuildCommand;
53
+ }
54
+ if (state.current_stage === 'review'
55
+ && state.review_verdict === 'request-changes'
56
+ && state.requested_transition === 'review->plan'
57
+ && state.approval?.rollback === 'approved') {
58
+ return `$plan ${state.slug}`;
10
59
  }
11
- if (state.current_stage === 'build' && Array.isArray(state.build_blockers) && state.build_blockers.length === 0) {
12
- return `$review ${state.slug}`;
60
+ if (state.current_stage === 'review'
61
+ && state.review_verdict === 'request-changes'
62
+ && state.requested_transition === 'review->clarify'
63
+ && state.approval?.rollback === 'approved') {
64
+ return `$clarify ${state.slug}`;
13
65
  }
14
66
  return null;
15
67
  }