@ai-content-space/loopx 0.1.3 → 0.1.5

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 (40) hide show
  1. package/README.md +123 -6
  2. package/README.zh-CN.md +143 -10
  3. package/assets/logo.svg +89 -0
  4. package/package.json +4 -2
  5. package/plugins/loopx/.codex-plugin/plugin.json +1 -1
  6. package/plugins/loopx/scripts/plugin-install.test.mjs +13 -0
  7. package/plugins/loopx/skills/archive/SKILL.md +14 -1
  8. package/plugins/loopx/skills/autopilot/SKILL.md +4 -1
  9. package/plugins/loopx/skills/build/SKILL.md +7 -1
  10. package/plugins/loopx/skills/clarify/SKILL.md +13 -9
  11. package/plugins/loopx/skills/debug/SKILL.md +4 -1
  12. package/plugins/loopx/skills/go-style/SKILL.md +4 -1
  13. package/plugins/loopx/skills/kratos/SKILL.md +4 -1
  14. package/plugins/loopx/skills/plan/SKILL.md +8 -4
  15. package/plugins/loopx/skills/review/SKILL.md +7 -1
  16. package/plugins/loopx/skills/tdd/SKILL.md +4 -1
  17. package/plugins/loopx/skills/verify/SKILL.md +4 -1
  18. package/scripts/codex-workflow-hook.mjs +101 -6
  19. package/scripts/verify-skills.mjs +166 -0
  20. package/skills/RESOLVER.md +45 -0
  21. package/skills/archive/SKILL.md +14 -1
  22. package/skills/autopilot/SKILL.md +4 -1
  23. package/skills/build/SKILL.md +7 -1
  24. package/skills/clarify/SKILL.md +13 -9
  25. package/skills/debug/SKILL.md +4 -1
  26. package/skills/go-style/SKILL.md +4 -1
  27. package/skills/kratos/SKILL.md +4 -1
  28. package/skills/plan/SKILL.md +8 -4
  29. package/skills/review/SKILL.md +7 -1
  30. package/skills/tdd/SKILL.md +4 -1
  31. package/skills/verify/SKILL.md +4 -1
  32. package/src/build-runtime.mjs +8 -0
  33. package/src/cli.mjs +10 -0
  34. package/src/context-manifest.mjs +3 -1
  35. package/src/html-views.mjs +316 -0
  36. package/src/plan-runtime.mjs +23 -0
  37. package/src/project-discovery.mjs +163 -0
  38. package/src/review-runtime.mjs +203 -23
  39. package/src/runtime-maintenance.mjs +1 -0
  40. package/src/workflow.mjs +499 -94
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ai-content-space/loopx",
3
3
  "type": "module",
4
- "version": "0.1.3",
4
+ "version": "0.1.5",
5
5
  "description": "Skill-first loopx workflow product for Codex",
6
6
  "repository": {
7
7
  "type": "git",
@@ -22,8 +22,10 @@
22
22
  "README.zh-CN.md",
23
23
  "package.json",
24
24
  "scripts/install-skills.mjs",
25
+ "scripts/verify-skills.mjs",
25
26
  "scripts/codex-stop-hook.mjs",
26
27
  "scripts/codex-workflow-hook.mjs",
28
+ "assets/logo.svg",
27
29
  "src/",
28
30
  "skills/",
29
31
  "templates/",
@@ -34,6 +36,6 @@
34
36
  },
35
37
  "scripts": {
36
38
  "postinstall": "node scripts/install-skills.mjs",
37
- "test": "node --test test/*.test.mjs"
39
+ "test": "node scripts/verify-skills.mjs && node --test test/*.test.mjs"
38
40
  }
39
41
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loopx",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Skill-first loopx workflow product for Codex",
5
5
  "skills": "./skills/",
6
6
  "interface": {
@@ -83,6 +83,19 @@ describe('loopx plugin shell', () => {
83
83
  assert.equal(ralplanSkill.includes('$plan --consensus'), false);
84
84
  });
85
85
 
86
+ it('locks clarify handoff to plan instead of direct implementation', async () => {
87
+ const clarifySkill = await readFile(join(ROOT_SKILLS_DIR, 'clarify', 'SKILL.md'), 'utf8');
88
+ const pluginClarifySkill = await readFile(join(PLUGIN_SKILLS_DIR, 'clarify', 'SKILL.md'), 'utf8');
89
+
90
+ assert.equal(pluginClarifySkill, clarifySkill);
91
+ assert.match(clarifySkill, /Recommended invocation: `\$plan <slug>`/);
92
+ assert.doesNotMatch(clarifySkill, /hand off to `build` only/i);
93
+ assert.doesNotMatch(clarifySkill, /direct execution/i);
94
+ assert.doesNotMatch(clarifySkill, /direct implementation/i);
95
+ assert.doesNotMatch(clarifySkill, /directly to implementation/i);
96
+ assert.doesNotMatch(clarifySkill, /Proceed directly to implementation/i);
97
+ });
98
+
86
99
  it('reuses the shared install core while materializing skills from the plugin shell', async () => {
87
100
  const home = await mkdtemp(join(tmpdir(), 'loopx-plugin-home-'));
88
101
  const env = loopxEnv(home);
@@ -1,6 +1,9 @@
1
1
  ---
2
2
  name: archive
3
- description: Sync an approved loopx change delta into long-lived specs after review is done.
3
+ description: "Archives an approved loopx change delta into long-lived specs and writes an ADR candidate after done approval. Not for active builds or unapproved reviews."
4
+ when_to_use: "archive, done workflow, spec delta, long-lived specs, ADR candidate, review approved, 归档, 同步规格"
5
+ metadata:
6
+ version: "0.1.5"
4
7
  argument-hint: "<workflow slug>"
5
8
  ---
6
9
 
@@ -10,6 +13,15 @@ argument-hint: "<workflow slug>"
10
13
 
11
14
  Use `archive` after a loopx workflow has reached `done`. It syncs the accepted change delta into long-lived `.loopx/specs/` files, archives the change staging directory, and writes an advisory ADR candidate.
12
15
 
16
+ The accepted delta is requirement-based, not a changelog block. Archive applies:
17
+
18
+ - `## ADDED Requirements`
19
+ - `## MODIFIED Requirements`
20
+ - `## REMOVED Requirements`
21
+ - `## RENAMED Requirements`
22
+
23
+ into the current long-lived `## Requirements` state for each target domain.
24
+
13
25
  ## Inputs
14
26
 
15
27
  - `<workflow slug>` for a completed loopx workflow
@@ -33,6 +45,7 @@ Then report in Chinese:
33
45
  ## Boundaries
34
46
 
35
47
  - Do not run archive before `review -> done` has been approved.
48
+ - Do not archive malformed requirement deltas. ADDED and MODIFIED entries must use `### Requirement:`, SHALL/MUST language, and at least one `#### Scenario:`.
36
49
  - Do not archive when `execution-record.md` declares non-empty `remaining_scope`, `completion_claim` other than `full`, or a mismatch between `planned_scope` and `implemented_scope`; route back to build/plan instead.
37
50
  - Do not edit implementation code.
38
51
  - Do not promote ADR candidates into `docs/adr/` automatically; report the candidate path for human follow-up.
@@ -1,6 +1,9 @@
1
1
  ---
2
2
  name: autopilot
3
- description: Richer internal-phase loopx autonomous orchestration over the public clarify/plan/build/review flow.
3
+ description: "Runs one bounded autonomous loopx orchestration over clarify, plan, build, and review while preserving canonical artifacts. Not for manual gate-by-gate control."
4
+ when_to_use: "autopilot, autonomous loopx run, end-to-end workflow, run all stages, bounded orchestration, 自动执行, 全流程"
5
+ metadata:
6
+ version: "0.1.5"
4
7
  argument-hint: "<workflow slug>"
5
8
  ---
6
9
 
@@ -1,6 +1,9 @@
1
1
  ---
2
2
  name: build
3
- description: Ralph-style loopx execution runtime under the public build stage.
3
+ description: "Executes an approved loopx plan or review rework contract with evidence, verification, deslop, and regression gates. Not for unclear requirements or independent review."
4
+ when_to_use: "build, implement approved plan, execute PRD, --from-review, review rework, implementation fixes, 执行, 实现, 修改"
5
+ metadata:
6
+ version: "0.1.5"
4
7
  argument-hint: "[--no-deslop] <approved PRD path or workflow slug> | --from-review <review artifact path>"
5
8
  ---
6
9
 
@@ -30,6 +33,7 @@ By default, `build` is not a one-shot draft writer. It is a persistence loop wit
30
33
  - Execution may parallelize internally without exposing a public `team` stage.
31
34
  - `build` does not replace `review`.
32
35
  - `execution-record.md` remains the sole canonical execution and verification artifact.
36
+ - `.loopx/config.json` is supporting context for existing project AI rules, existing spec sources, and discovered verification commands; use it to preserve local sources of truth, not to skip loopx stages.
33
37
  - Feature work and bug fixes should use `tdd`: write a failing test, confirm it fails for the intended reason, then implement the smallest passing change.
34
38
  - Bug, test-failure, build-failure, and unexpected-behavior work should use `debug` before proposing fixes.
35
39
  - Completion and review-ready claims should use `verify` before they are stated.
@@ -72,6 +76,8 @@ Compatible skill / CLI input:
72
76
  When invoked with a PRD path, derive `<slug>` from `prd-<slug>.md` and still use the matching workflow-local plan package and test spec.
73
77
 
74
78
  When invoked with `--from-review`, derive `<slug>` from the workflow directory, treat the review artifact as the implementation-fix contract, and load the matching PRD, test spec, previous `execution-record.md`, and workflow-local plan package as supporting context. This Codex skill invocation consumes the `review -> build` rework intent; users should not need a separate bash `loopx approve ... --from review --to build` step for the normal Codex-facing flow.
79
+
80
+ Also load `.loopx/config.json` when present. Its `project_conventions` entries identify existing project rule/spec files that should be preserved, and its `verification_commands` entries are the first project-native commands to consider for fresh evidence.
75
81
  </Inputs>
76
82
 
77
83
  <Execution_Model>
@@ -1,6 +1,9 @@
1
1
  ---
2
2
  name: clarify
3
- description: Comprehensive loopx clarify skill for requirements, boundaries, and design-ready specs.
3
+ description: "Clarifies ambiguous loopx work into requirements, non-goals, decision boundaries, and design-ready specs before planning. Not for already-approved plans or concrete implementation tasks."
4
+ when_to_use: "clarify, requirements, ambiguous request, unclear scope, non-goals, decision boundaries, acceptance criteria, 需求澄清, 范围不清"
5
+ metadata:
6
+ version: "0.1.5"
4
7
  ---
5
8
 
6
9
  # loopx Clarify
@@ -23,7 +26,7 @@ Its job is not just to ask questions. Its job is to turn a vague or overloaded r
23
26
  - The request is broad, ambiguous, or mixes problem, solution, and implementation detail.
24
27
  - The user needs help defining scope, non-goals, acceptance criteria, or tradeoffs before planning.
25
28
  - A design direction exists only implicitly and would otherwise be invented during implementation.
26
- - The task will later be handed to `plan`, `build`, or `autopilot`, and you want a high-signal spec first.
29
+ - The task will later be handed to `plan`, and you want a high-signal spec first.
27
30
  </Use_When>
28
31
 
29
32
  <Do_Not_Use_When>
@@ -33,7 +36,7 @@ Its job is not just to ask questions. Its job is to turn a vague or overloaded r
33
36
  </Do_Not_Use_When>
34
37
 
35
38
  <Why_This_Exists>
36
- Most implementation drift happens before coding begins. Teams often think they need “more planning,” when the real problem is weaker intent clarity, hidden assumptions, fuzzy boundaries, or a design shape that was never made explicit. `clarify` exists to solve those upstream failures before `plan` or `build` magnifies them.
39
+ Most implementation drift happens before coding begins. Teams often think they need “more planning,” when the real problem is weaker intent clarity, hidden assumptions, fuzzy boundaries, or a design shape that was never made explicit. `clarify` exists to solve those upstream failures before `plan` turns the clarified intent into an execution contract.
37
40
  </Why_This_Exists>
38
41
 
39
42
  <Core_Principles>
@@ -268,7 +271,7 @@ Before marking a clarify spec handoff-ready, perform a self-review pass:
268
271
 
269
272
  Write the output to the loopx runtime namespace:
270
273
 
271
- - `.loopx/specs/clarify-<slug>-<timestamp>.md`
274
+ - `.loopx/intake/clarify-<slug>-<timestamp>.md`
272
275
 
273
276
  The clarify spec should include:
274
277
 
@@ -299,17 +302,17 @@ The clarify spec should include:
299
302
 
300
303
  After the clarify spec is ready:
301
304
 
302
- - hand off to `plan` by default
303
- - hand off to `build` only if the user explicitly wants direct execution and the task is already concrete enough
304
- - hand off to `autopilot` only when the scope is sufficiently tight for a bounded end-to-end run
305
+ - hand off to `plan`; do not start implementation, TDD, `build`, or `autopilot` from `clarify`
306
+ - if the user asks to execute immediately, explain that loopx requires the `plan` gate first and provide the plan invocation
307
+ - if a task is too small to justify planning, do not use `clarify`; handle that request outside the clarify workflow from the start
305
308
 
306
309
  Preferred explicit handoff contract:
307
310
 
308
311
  - Recommended invocation: `$plan <slug>`
309
- - Artifact-pinned invocation when needed: `$plan --direct .loopx/specs/clarify-<slug>-<timestamp>.md`
312
+ - Artifact-pinned invocation when needed: `$plan --direct .loopx/intake/clarify-<slug>-<timestamp>.md`
310
313
  - Consumer behavior: treat the clarify spec as the source of truth for intent, non-goals, decision boundaries, constraints, and design direction; do not reopen clarification by default
311
314
 
312
- `clarify` itself does not implement the feature.
315
+ `clarify` itself does not implement the feature. The handoff recommendation must name `plan` as the next workflow step.
313
316
 
314
317
  </Process>
315
318
 
@@ -328,6 +331,7 @@ Preferred explicit handoff contract:
328
331
 
329
332
  <Must_Not_Decide_Automatically>
330
333
  - approval to move from clarify into plan
334
+ - skipping `plan` after producing a clarify spec
331
335
  - implementation details that were never clarified or grounded
332
336
  - widening the task because a broader redesign sounds cleaner
333
337
  </Must_Not_Decide_Automatically>
@@ -1,6 +1,9 @@
1
1
  ---
2
2
  name: debug
3
- description: Use when encountering any bug, test failure, or unexpected behavior, before proposing fixes
3
+ description: "Finds root cause for bugs, failing tests, build failures, regressions, and unexpected behavior before fixes. Not for new feature planning or routine code review."
4
+ when_to_use: "debug, bug, test failure, build failure, regression, unexpected behavior, root cause, 报错, 失败, 回归, 排查"
5
+ metadata:
6
+ version: "0.1.5"
4
7
  ---
5
8
 
6
9
  # Systematic Debugging
@@ -1,6 +1,9 @@
1
1
  ---
2
2
  name: go-style
3
- description: Go language style support for loopx. Use before creating or editing Go (.go) files, especially inside build execution lanes.
3
+ description: "Applies loopx Go coding style for .go edits, tests, errors, context, naming, and interface boundaries. Not for non-Go code or Kratos-specific architecture by itself."
4
+ when_to_use: "go-style, Go, golang, .go files, go tests, gofmt, idiomatic Go, Go style, Go 代码"
5
+ metadata:
6
+ version: "0.1.5"
4
7
  ---
5
8
 
6
9
  # Go Style
@@ -1,6 +1,9 @@
1
1
  ---
2
2
  name: kratos
3
- description: Go-Kratos framework support for loopx. Use for Kratos microservices, proto/buf API work, service/biz/data layering, Kratos middleware, auth, configuration, and troubleshooting.
3
+ description: "Supports Go-Kratos microservices, proto/buf APIs, service/biz/data layers, middleware, auth, config, and troubleshooting. Not for generic Go style alone."
4
+ when_to_use: "kratos, Go-Kratos, proto, buf, service layer, biz layer, data layer, middleware, auth, config, Kratos 微服务"
5
+ metadata:
6
+ version: "0.1.5"
4
7
  ---
5
8
 
6
9
  # Kratos
@@ -1,6 +1,9 @@
1
1
  ---
2
2
  name: plan
3
- description: Consensus-first loopx planning skill with Planner, Architect, and Critic review.
3
+ description: "Creates a consensus-first loopx plan package with Planner, Architect, and Critic review from an approved spec. Not for unresolved requirements or direct implementation."
4
+ when_to_use: "plan, planning, consensus planning, PRD, architecture plan, test plan, approved clarify spec, 规划, 方案, 架构评审"
5
+ metadata:
6
+ version: "0.1.5"
4
7
  argument-hint: "[--interactive] [--deliberate] [--direct <spec-path>] <clarified task or spec path>"
5
8
  ---
6
9
 
@@ -40,7 +43,7 @@ By default, `plan` includes the full consensus review loop formerly documented u
40
43
  Accepted inputs:
41
44
 
42
45
  - an approved loopx clarify workflow slug
43
- - `.loopx/specs/clarify-*.md`
46
+ - `.loopx/intake/clarify-*.md`
44
47
  - `.omx/specs/deep-interview-*.md`
45
48
  - a direct task description when enough context is already present
46
49
  - `--direct <spec-path>` to force a specific requirements artifact
@@ -174,7 +177,7 @@ The final plan must include:
174
177
 
175
178
  - ADR: Decision, Drivers, Alternatives considered, Why chosen, Consequences, Follow-ups
176
179
  - concrete implementation steps sized to the actual task
177
- - target long-lived spec domains and the requirements delta for archive
180
+ - target long-lived spec domains and an OpenSpec-style requirements delta for archive
178
181
  - vertical slices sized as independently verifiable tracer bullets, not horizontal layer-only task groups
179
182
  - execution inputs mapped to concrete sources before build starts
180
183
  - available execution lanes and recommended lane
@@ -220,7 +223,8 @@ The plan gate is blocked until:
220
223
 
221
224
  - plan package artifacts exist
222
225
  - change proposal, spec delta, design, tasks, vertical slices, and artifact graph exist
223
- - spec delta declares target domains and added or modified requirements
226
+ - spec delta declares target domains and `## ADDED|MODIFIED|REMOVED|RENAMED Requirements` blocks
227
+ - every ADDED or MODIFIED requirement uses `### Requirement:`, contains SHALL or MUST text, and includes at least one `#### Scenario:`
224
228
  - vertical slices contain at least one `AFK` or `HITL` end-to-end slice with acceptance criteria and verification signal
225
229
  - Architect review is complete
226
230
  - Critic verdict is `approve`
@@ -1,6 +1,9 @@
1
1
  ---
2
2
  name: review
3
- description: Repo-local acceptance surface for loopx.
3
+ description: "Reviews a loopx build execution record for acceptance, code risks, evidence quality, and architecture smells. Not for doing implementation work or replanning."
4
+ when_to_use: "review, code review, acceptance, go no-go, execution-record, architecture smell, build complete, 审查, 验收"
5
+ metadata:
6
+ version: "0.1.5"
4
7
  argument-hint: "<execution-record path or workflow slug>"
5
8
  ---
6
9
 
@@ -22,6 +25,8 @@ Compatible skill / CLI input:
22
25
 
23
26
  When invoked with an execution record path, derive `<slug>` from the workflow directory and evaluate the matching active run.
24
27
 
28
+ When present, use `.loopx/config.json` as supporting context for project-native verification commands, existing AI rule files, and existing spec sources. Do not treat those external or pre-existing sources as replacements for the loopx execution record and review artifact.
29
+
25
30
  ## Expected Outputs
26
31
 
27
32
  - a review artifact tied to the run being evaluated
@@ -42,6 +47,7 @@ Use stable machine values only where they are commands, file paths, JSON/state f
42
47
  - Use this only after build has produced execution and verification evidence for a specific run.
43
48
  - Stop here if review evidence is incomplete. `review` remains an independent gate and does not auto-complete the workflow.
44
49
  - Review must include code review of the build-owned implementation diff. Do not limit review to artifact/schema checks.
50
+ - Review should compare verification evidence against project-native commands recorded in `.loopx/config.json` when available, while still accepting stronger task-specific verification from the approved plan.
45
51
  - Review must include the architecture-smell lane as part of review evidence. This is not a new workflow stage and must not create extra user steps.
46
52
  - Review must compare the execution scope against the approved workflow scope. If `execution-record.md` declares non-empty `remaining_scope`, `completion_claim` other than `full`, or a mismatch between `planned_scope` and `implemented_scope`, review must return no-go and route to build or plan. A partial slice may be accepted as useful work, but it must not be approved as full workflow completion.
47
53
  - Code review findings should focus on real bugs, regressions, missing tests, broken contracts, security/data-integrity risks, and user-visible behavior gaps.
@@ -1,6 +1,9 @@
1
1
  ---
2
2
  name: tdd
3
- description: Use when implementing any feature or bugfix, before writing implementation code
3
+ description: "Guides feature and bugfix implementation through a failing test before production code and red-green-refactor discipline. Not for generated files or throwaway prototypes."
4
+ when_to_use: "tdd, failing test first, feature implementation, bugfix, regression test, red green refactor, 测试先行"
5
+ metadata:
6
+ version: "0.1.5"
4
7
  ---
5
8
 
6
9
  # Test-Driven Development (TDD)
@@ -1,6 +1,9 @@
1
1
  ---
2
2
  name: verify
3
- description: Use when about to claim work is complete, fixed, or passing, before committing or creating PRs - requires running verification commands and confirming output before making any success claims; evidence before assertions always
3
+ description: "Requires fresh verification evidence before claiming work is complete, fixed, passing, review-ready, or ready to commit. Not for speculative confidence or stale results."
4
+ when_to_use: "verify, completion claim, fixed claim, tests pass, review-ready, commit, fresh evidence, 验证, 完成前检查"
5
+ metadata:
6
+ version: "0.1.5"
4
7
  ---
5
8
 
6
9
  # Verification Before Completion
@@ -3,7 +3,6 @@
3
3
  import { existsSync, readdirSync } from 'node:fs';
4
4
  import { readFile } from 'node:fs/promises';
5
5
  import { dirname, join, resolve } from 'node:path';
6
- import { nextSkillCommand } from '../src/next-skill.mjs';
7
6
 
8
7
  function readStdin() {
9
8
  return new Promise((resolveValue) => {
@@ -29,9 +28,61 @@ function readStdin() {
29
28
  }
30
29
 
31
30
  function nextSkill(state) {
32
- const command = nextSkillCommand(state);
33
- if (command) {
34
- return command;
31
+ if (!state || !state.slug) {
32
+ return null;
33
+ }
34
+ const reviewBuildCommand = `$build --from-review .loopx/workflows/${state.slug}/review-report.md`;
35
+ if (isClarifyReadyForPlan(state)) {
36
+ return `$plan ${state.slug}`;
37
+ }
38
+ if (state.current_stage === 'done'
39
+ && state.completion_confirmed === true
40
+ && state.archive_status !== 'archived') {
41
+ return `$archive ${state.slug}`;
42
+ }
43
+ if (state.stage_status === 'awaiting-approval'
44
+ && state.current_stage === 'plan'
45
+ && Array.isArray(state.plan_blockers)
46
+ && state.plan_blockers.length === 0) {
47
+ return `$build .loopx/plans/prd-${state.slug}.md`;
48
+ }
49
+ if (state.current_stage === 'build'
50
+ && state.stage_status === 'awaiting-approval'
51
+ && state.pending_user_decision === 'build->review'
52
+ && state.review_status === 'ready-for-review'
53
+ && state.execution_record_status === 'complete'
54
+ && Array.isArray(state.build_blockers)
55
+ && state.build_blockers.length === 0) {
56
+ return `$review .loopx/workflows/${state.slug}/execution-record.md`;
57
+ }
58
+ if (state.current_stage === 'review'
59
+ && state.review_verdict === 'request-changes'
60
+ && state.rollback_target === 'build'
61
+ && (
62
+ state.pending_user_decision === 'review->build'
63
+ || state.requested_transition === 'review->build'
64
+ || state.approval?.build === 'requested'
65
+ || state.approval?.build === 'approved'
66
+ )) {
67
+ return reviewBuildCommand;
68
+ }
69
+ if (state.current_stage === 'review'
70
+ && state.review_verdict === 'request-changes'
71
+ && state.requested_transition === 'review->build'
72
+ && state.approval?.build === 'approved') {
73
+ return reviewBuildCommand;
74
+ }
75
+ if (state.current_stage === 'review'
76
+ && state.review_verdict === 'request-changes'
77
+ && state.requested_transition === 'review->plan'
78
+ && state.approval?.rollback === 'approved') {
79
+ return `$plan ${state.slug}`;
80
+ }
81
+ if (state.current_stage === 'review'
82
+ && state.review_verdict === 'request-changes'
83
+ && state.requested_transition === 'review->clarify'
84
+ && state.approval?.rollback === 'approved') {
85
+ return `$clarify ${state.slug}`;
35
86
  }
36
87
  if (state.current_stage === 'review' && state.review_verdict === 'approve' && state.pending_user_decision === 'review->done') {
37
88
  return `loopx approve ${state.slug} --from review --to done`;
@@ -59,6 +110,49 @@ function stateLine(key, value) {
59
110
  return `${key}: ${value ?? 'unknown'}`;
60
111
  }
61
112
 
113
+ function isClarifyReadyForPlan(state) {
114
+ return (state.current_stage === 'clarify' || (!state.current_stage && typeof state.clarify_current_round === 'number'))
115
+ && state.clarify_current_round > 0
116
+ && state.unresolved_ambiguity_count === 0
117
+ && state.clarify_non_goals_resolved === true
118
+ && state.clarify_decision_boundaries_resolved === true
119
+ && state.clarify_pressure_pass_complete === true
120
+ && typeof state.clarify_ambiguity_score === 'number'
121
+ && typeof state.clarify_target_ambiguity_threshold === 'number'
122
+ && state.clarify_ambiguity_score <= state.clarify_target_ambiguity_threshold;
123
+ }
124
+
125
+ function isLegacyClarifyState(state) {
126
+ return !state.current_stage && typeof state.clarify_current_round === 'number';
127
+ }
128
+
129
+ function nextActionLine(state, workflow) {
130
+ if (isLegacyClarifyState(state) && isClarifyReadyForPlan(state)) {
131
+ return `loopx migrate, then $plan ${state.slug || workflow}`;
132
+ }
133
+ if (isClarifyReadyForPlan(state) && state.approval?.plan !== 'approved') {
134
+ return `approve clarify -> plan, then $plan ${state.slug || workflow}`;
135
+ }
136
+ return nextSkill(state) || state.recommended_next_action || 'none';
137
+ }
138
+
139
+ function implementationGateLines(state) {
140
+ if (isClarifyReadyForPlan(state) && state.approval?.build !== 'approved') {
141
+ return [
142
+ 'implementation gate: blocked until plan is approved',
143
+ 'do not start build, TDD, or code edits from clarify',
144
+ ];
145
+ }
146
+ return [];
147
+ }
148
+
149
+ function stageText(state) {
150
+ if (isLegacyClarifyState(state)) {
151
+ return `legacy-clarify (${isClarifyReadyForPlan(state) ? 'blocked' : 'incomplete'})`;
152
+ }
153
+ return `${state.current_stage || 'unknown'} (${state.stage_status || 'unknown'})`;
154
+ }
155
+
62
156
  function evidenceLines(state) {
63
157
  const evidence = Array.isArray(state.current_evidence_chain) ? state.current_evidence_chain : [];
64
158
  if (evidence.length === 0) {
@@ -131,9 +225,10 @@ try {
131
225
  '</loopx_instructions>',
132
226
  '<loopx_state>',
133
227
  `loopx workflow: ${state.slug || workflow}`,
134
- `stage: ${state.current_stage || 'unknown'} (${state.stage_status || 'unknown'})`,
135
- `next: ${nextSkill(state) || state.recommended_next_action || 'none'}`,
228
+ `stage: ${stageText(state)}`,
229
+ `next: ${nextActionLine(state, workflow)}`,
136
230
  `blockers: ${blockers(state)}`,
231
+ ...implementationGateLines(state),
137
232
  `approval: ${JSON.stringify(state.approval || {})}`,
138
233
  stateLine('readiness.plan.ready', boolText(state.readiness?.plan?.ready)),
139
234
  stateLine('readiness.build.ready', boolText(state.readiness?.build?.ready)),
@@ -0,0 +1,166 @@
1
+ #!/usr/bin/env node
2
+
3
+ import assert from 'node:assert/strict';
4
+ import { existsSync } from 'node:fs';
5
+ import { readdir, readFile } from 'node:fs/promises';
6
+ import { dirname, join, resolve } from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+
9
+ import { LOOPX_BUNDLED_SKILLS } from '../src/install-discovery.mjs';
10
+
11
+ const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
12
+ const packageJson = JSON.parse(await readFile(join(repoRoot, 'package.json'), 'utf8'));
13
+ const pluginManifest = JSON.parse(await readFile(join(repoRoot, 'plugins', 'loopx', '.codex-plugin', 'plugin.json'), 'utf8'));
14
+ const resolverPath = join(repoRoot, 'skills', 'RESOLVER.md');
15
+ const markdownPaths = [
16
+ 'README.md',
17
+ 'README.zh-CN.md',
18
+ 'AGENTS.md',
19
+ 'skills/RESOLVER.md',
20
+ ];
21
+ const personalPathPattern = /\/(?:Users|home)\/[A-Za-z0-9._-]+\//;
22
+ const localRefPattern = /(?<![/.])\b(?:references|agents|scripts)\/[\w/.-]+\b/g;
23
+
24
+ function parseFrontmatter(path, text) {
25
+ assert.equal(text.startsWith('---\n'), true, `${path} must start with YAML frontmatter`);
26
+ const end = text.indexOf('\n---\n', 4);
27
+ assert.notEqual(end, -1, `${path} frontmatter must close with ---`);
28
+
29
+ const fields = {};
30
+ let inMetadata = false;
31
+ for (const line of text.slice(4, end).split('\n')) {
32
+ if (line === 'metadata:') {
33
+ inMetadata = true;
34
+ continue;
35
+ }
36
+ if (inMetadata && line.startsWith(' version:')) {
37
+ fields.version = line.split(':', 2)[1].trim().replace(/^"|"$/g, '');
38
+ continue;
39
+ }
40
+ if (!line || line.startsWith(' ')) {
41
+ continue;
42
+ }
43
+ inMetadata = false;
44
+ const separator = line.indexOf(':');
45
+ assert.notEqual(separator, -1, `${path} has invalid frontmatter line: ${line}`);
46
+ const key = line.slice(0, separator).trim();
47
+ const raw = line.slice(separator + 1).trim();
48
+ fields[key] = raw.replace(/^"|"$/g, '');
49
+ }
50
+ return fields;
51
+ }
52
+
53
+ function assertSkillDescription(skillName, description) {
54
+ assert.ok(description, `${skillName} missing description`);
55
+ assert.ok(description.length >= 40, `${skillName} description is too short`);
56
+ assert.ok(description.length <= 500, `${skillName} description is too long`);
57
+ assert.match(description, /not for/i, `${skillName} description must include a Not for exclusion`);
58
+ }
59
+
60
+ async function assertMarkdownStructure(relativePath) {
61
+ const path = join(repoRoot, relativePath);
62
+ assert.equal(existsSync(path), true, `${relativePath} missing`);
63
+ const text = await readFile(path, 'utf8');
64
+ assert.equal(text.endsWith('\n'), true, `${relativePath} missing final newline`);
65
+
66
+ const fenceStack = [];
67
+ text.split('\n').forEach((line, index) => {
68
+ assert.equal(/^(<<<<<<<|=======|>>>>>>>)($| )/.test(line), false, `${relativePath}:${index + 1}: merge conflict marker`);
69
+ const match = line.match(/^(`{3,}|~{3,})/);
70
+ if (!match) {
71
+ return;
72
+ }
73
+ const marker = match[1];
74
+ if (fenceStack.length > 0 && marker[0] === fenceStack.at(-1).char && marker.length >= fenceStack.at(-1).length) {
75
+ fenceStack.pop();
76
+ return;
77
+ }
78
+ fenceStack.push({ char: marker[0], length: marker.length, line: index + 1 });
79
+ });
80
+
81
+ assert.deepEqual(fenceStack, [], `${relativePath} has unclosed fenced block`);
82
+ }
83
+
84
+ function assertContains(text, value, label) {
85
+ assert.match(text, new RegExp(value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')), `${label} missing ${value}`);
86
+ }
87
+
88
+ async function assertPublicDocsAligned() {
89
+ const readme = await readFile(join(repoRoot, 'README.md'), 'utf8');
90
+ const readmeZh = await readFile(join(repoRoot, 'README.zh-CN.md'), 'utf8');
91
+ const commands = [
92
+ 'loopx init',
93
+ 'loopx clarify',
94
+ 'loopx approve',
95
+ 'loopx plan',
96
+ 'loopx build',
97
+ 'loopx review',
98
+ 'loopx archive',
99
+ 'loopx autopilot',
100
+ 'loopx render',
101
+ 'loopx status',
102
+ 'loopx setup-context',
103
+ 'loopx doctor',
104
+ 'loopx migrate',
105
+ 'loopx repair-install',
106
+ 'node scripts/verify-skills.mjs',
107
+ ];
108
+ for (const command of commands) {
109
+ assertContains(readme, command, 'README.md');
110
+ assertContains(readmeZh, command, 'README.zh-CN.md');
111
+ }
112
+
113
+ const releaseNotesRoot = join(repoRoot, 'docs', 'release-notes');
114
+ const releaseNotes = existsSync(releaseNotesRoot)
115
+ ? (await readdir(releaseNotesRoot)).filter((name) => name.endsWith('.md'))
116
+ : [];
117
+ assert.ok(releaseNotes.includes(`${packageJson.version}.md`), `docs/release-notes/${packageJson.version}.md missing`);
118
+ for (const name of releaseNotes) {
119
+ await assertMarkdownStructure(`docs/release-notes/${name}`);
120
+ }
121
+ }
122
+
123
+ async function assertSkill(skillName, resolverText) {
124
+ const rootPath = join(repoRoot, 'skills', skillName, 'SKILL.md');
125
+ const pluginPath = join(repoRoot, 'plugins', 'loopx', 'skills', skillName, 'SKILL.md');
126
+ assert.equal(existsSync(rootPath), true, `${skillName} root SKILL.md missing`);
127
+ assert.equal(existsSync(pluginPath), true, `${skillName} plugin SKILL.md missing`);
128
+
129
+ const rootText = await readFile(rootPath, 'utf8');
130
+ const pluginText = await readFile(pluginPath, 'utf8');
131
+ assert.equal(pluginText, rootText, `${skillName} plugin mirror drifted`);
132
+ assert.equal(personalPathPattern.test(rootText), false, `${skillName} contains a personal absolute path`);
133
+
134
+ const fields = parseFrontmatter(rootPath, rootText);
135
+ assert.equal(fields.name, skillName, `${skillName} frontmatter name mismatch`);
136
+ assert.equal(fields.version, packageJson.version, `${skillName} metadata.version must match package.json`);
137
+ assert.ok(fields.when_to_use && fields.when_to_use.length >= 20, `${skillName} missing useful when_to_use metadata`);
138
+ assertSkillDescription(skillName, fields.description);
139
+ assert.match(resolverText, new RegExp(`skills/${skillName}/SKILL\\.md`), `${skillName} missing from skills/RESOLVER.md`);
140
+
141
+ const refs = [...rootText.matchAll(localRefPattern)].map((match) => match[0]);
142
+ for (const ref of refs) {
143
+ const target = join(repoRoot, 'skills', skillName, ref);
144
+ assert.equal(existsSync(target), true, `${skillName} references missing local file: ${ref}`);
145
+ }
146
+ }
147
+
148
+ assert.equal(pluginManifest.version, packageJson.version, 'plugin manifest version must match package.json');
149
+ assert.equal(existsSync(resolverPath), true, 'skills/RESOLVER.md missing');
150
+
151
+ for (const relativePath of markdownPaths) {
152
+ await assertMarkdownStructure(relativePath);
153
+ }
154
+ await assertPublicDocsAligned();
155
+
156
+ const resolverText = await readFile(resolverPath, 'utf8');
157
+ for (const skillName of LOOPX_BUNDLED_SKILLS) {
158
+ await assertSkill(skillName, resolverText);
159
+ }
160
+
161
+ const staleRefs = [...resolverText.matchAll(/skills\/([a-z][a-z0-9-]*)\/SKILL\.md/g)]
162
+ .map((match) => match[1])
163
+ .filter((skillName) => !LOOPX_BUNDLED_SKILLS.includes(skillName));
164
+ assert.deepEqual([...new Set(staleRefs)], [], 'skills/RESOLVER.md contains stale bundled-skill refs');
165
+
166
+ console.log(`ok: verified ${LOOPX_BUNDLED_SKILLS.length} loopx bundled skills`);