ll-skills 2.0.2 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +42 -20
  3. package/agents/ll-executor.md +2 -1
  4. package/agents/ll-verifier.md +1 -0
  5. package/assets/preamble.md +27 -39
  6. package/bin/install.js +4 -1
  7. package/hooks/ll-precompact.js +29 -1
  8. package/hooks/ll-skills-check-update.js +6 -6
  9. package/hooks/ll-state.js +30 -2
  10. package/package.json +3 -2
  11. package/scripts/evals/README.md +81 -0
  12. package/scripts/evals/cases/auto-dry-run/assert.sh +35 -0
  13. package/scripts/evals/cases/auto-dry-run/case.json +8 -0
  14. package/scripts/evals/cases/auto-dry-run/prompt.txt +1 -0
  15. package/scripts/evals/cases/auto-empty-repo/assert.sh +25 -0
  16. package/scripts/evals/cases/auto-empty-repo/case.json +8 -0
  17. package/scripts/evals/cases/auto-empty-repo/fixture/.gitkeep +0 -0
  18. package/scripts/evals/cases/auto-empty-repo/prompt.txt +1 -0
  19. package/scripts/evals/cases/decide-final-round/assert.sh +45 -0
  20. package/scripts/evals/cases/decide-final-round/case.json +8 -0
  21. package/scripts/evals/cases/decide-final-round/fixture/README.md +3 -0
  22. package/scripts/evals/cases/decide-final-round/prompt.txt +1 -0
  23. package/scripts/evals/cases/executor-block/assert.sh +33 -0
  24. package/scripts/evals/cases/executor-block/case.json +8 -0
  25. package/scripts/evals/cases/executor-block/prompt.txt +14 -0
  26. package/scripts/evals/cases/goal-autonomous/assert.sh +35 -0
  27. package/scripts/evals/cases/goal-autonomous/case.json +8 -0
  28. package/scripts/evals/cases/goal-autonomous/fixture/PLAN.md +42 -0
  29. package/scripts/evals/cases/goal-autonomous/fixture/PROGRESS.md +20 -0
  30. package/scripts/evals/cases/goal-autonomous/fixture/ROADMAP.md +29 -0
  31. package/scripts/evals/cases/goal-autonomous/fixture/package.json +8 -0
  32. package/scripts/evals/cases/goal-autonomous/fixture/src/money.js +6 -0
  33. package/scripts/evals/cases/goal-autonomous/fixture/test/reconcile.test.js +8 -0
  34. package/scripts/evals/cases/goal-autonomous/prompt.txt +1 -0
  35. package/scripts/evals/cases/implement-review-gate/assert.sh +37 -0
  36. package/scripts/evals/cases/implement-review-gate/case.json +8 -0
  37. package/scripts/evals/cases/implement-review-gate/prompt.txt +1 -0
  38. package/scripts/evals/cases/implement-stops-at-next/assert.sh +121 -0
  39. package/scripts/evals/cases/implement-stops-at-next/case.json +9 -0
  40. package/scripts/evals/cases/implement-stops-at-next/prompt.txt +1 -0
  41. package/scripts/evals/cases/preamble-no-ritual/assert.sh +17 -0
  42. package/scripts/evals/cases/preamble-no-ritual/case.json +8 -0
  43. package/scripts/evals/cases/preamble-no-ritual/fixture/README.md +3 -0
  44. package/scripts/evals/cases/preamble-no-ritual/fixture/src/a.ts +3 -0
  45. package/scripts/evals/cases/preamble-no-ritual/prompt.txt +1 -0
  46. package/scripts/evals/cases/router-no-skill/assert.sh +33 -0
  47. package/scripts/evals/cases/router-no-skill/case.json +8 -0
  48. package/scripts/evals/cases/router-no-skill/fixture/PLAN.md +21 -0
  49. package/scripts/evals/cases/router-no-skill/fixture/README.md +7 -0
  50. package/scripts/evals/cases/router-no-skill/fixture/decisions/README.md +3 -0
  51. package/scripts/evals/cases/router-no-skill/prompt.txt +1 -0
  52. package/scripts/evals/cases/router-small/assert.sh +21 -0
  53. package/scripts/evals/cases/router-small/case.json +8 -0
  54. package/scripts/evals/cases/router-small/fixture/README.md +17 -0
  55. package/scripts/evals/cases/router-small/prompt.txt +1 -0
  56. package/scripts/evals/cases/scout-no-plan/assert.sh +41 -0
  57. package/scripts/evals/cases/scout-no-plan/case.json +8 -0
  58. package/scripts/evals/cases/scout-no-plan/prompt.txt +8 -0
  59. package/scripts/evals/cases/verifier-weakened-test/assert.sh +19 -0
  60. package/scripts/evals/cases/verifier-weakened-test/case.json +8 -0
  61. package/scripts/evals/cases/verifier-weakened-test/prompt.txt +13 -0
  62. package/scripts/evals/cases/verifier-weakened-test/setup.sh +19 -0
  63. package/scripts/evals/fixtures/manual-contract/out.json +29 -0
  64. package/scripts/evals/fixtures/manual-contract/out.txt +5 -0
  65. package/scripts/evals/fixtures/manual-contract/with-skill.json +46 -0
  66. package/scripts/evals/fixtures/router-no-skill/fail.txt +5 -0
  67. package/scripts/evals/fixtures/router-no-skill/out.json +27 -0
  68. package/scripts/evals/fixtures/router-no-skill/pass.txt +4 -0
  69. package/scripts/evals/lib/assert.sh +107 -0
  70. package/scripts/evals/lib/extract.js +73 -0
  71. package/scripts/evals/run.sh +369 -0
  72. package/scripts/fixtures/auto-closed/PLAN.md +5 -0
  73. package/scripts/fixtures/auto-closed/PROGRESS.md +20 -0
  74. package/scripts/fixtures/auto-closed/ROADMAP.md +6 -0
  75. package/scripts/fixtures/auto-closed/docs/DELIVERY.md +3 -0
  76. package/scripts/fixtures/auto-decisions/decisions/DEC-0001-taken-alone.md +13 -0
  77. package/scripts/fixtures/auto-decisions/decisions/DEC-0002-owner.md +13 -0
  78. package/scripts/fixtures/auto-noroadmap/PLAN.md +20 -0
  79. package/scripts/fixtures/auto-noroadmap/PROGRESS.md +11 -0
  80. package/scripts/fixtures/auto-verify-next/PLAN.md +5 -0
  81. package/scripts/fixtures/auto-verify-next/PROGRESS.md +18 -0
  82. package/scripts/fixtures/auto-verify-next/ROADMAP.md +5 -0
  83. package/scripts/fixtures/auto-verify-next/phases/01/PLAN.md +6 -0
  84. package/scripts/fixtures/evals-auto/auto-dry-run/pass.txt +18 -0
  85. package/scripts/fixtures/evals-auto/auto-empty-repo/pass.txt +2 -0
  86. package/scripts/fixtures/evals-auto/goal-autonomous/pass.txt +29 -0
  87. package/scripts/fixtures/lint-bad/folded-description/SKILL.md +13 -0
  88. package/scripts/fixtures/lint-bad/jargon-in-questions/SKILL.md +36 -0
  89. package/scripts/fixtures/lint-bad/model-invocation-false/SKILL.md +10 -0
  90. package/scripts/fixtures/next-bad/skills/ll-bad/SKILL.md +30 -0
  91. package/scripts/fixtures/next-good/skills/ll-good/SKILL.md +26 -0
  92. package/scripts/fixtures/project/BACKLOG.md +6 -5
  93. package/scripts/fixtures/project/PROGRESS.md +4 -0
  94. package/scripts/lint-contract.cjs +495 -0
  95. package/scripts/lint-prompts.sh +443 -0
  96. package/scripts/ll-tools.js +497 -450
  97. package/scripts/smoke-test.sh +481 -4
  98. package/skills/ll-auto/SKILL.md +74 -0
  99. package/skills/ll-auto/references/run.md +75 -0
  100. package/skills/ll-auto/references/stages.md +66 -0
  101. package/skills/ll-auto/scripts/ll-auto.js +345 -0
  102. package/skills/ll-brainstorm/SKILL.md +14 -12
  103. package/skills/ll-brainstorm/references/decision-policy.md +22 -12
  104. package/skills/ll-close/SKILL.md +8 -7
  105. package/skills/ll-close/references/delivery.md +3 -1
  106. package/skills/ll-decide/SKILL.md +21 -19
  107. package/skills/ll-decide/references/decision-policy.md +22 -12
  108. package/skills/ll-decide/references/decision-room.md +5 -3
  109. package/skills/ll-decide/references/interview.md +29 -13
  110. package/skills/ll-decide/references/plan-skeleton.md +14 -14
  111. package/skills/ll-decide/references/premise-gate.md +34 -20
  112. package/skills/ll-goal/SKILL.md +22 -4
  113. package/skills/ll-goal/references/goal-template.md +57 -0
  114. package/skills/ll-implement/SKILL.md +25 -22
  115. package/skills/ll-implement/references/briefs.md +2 -1
  116. package/skills/ll-implement/references/decision-policy.md +22 -12
  117. package/skills/ll-implement/references/phase-conversation.md +20 -12
  118. package/skills/ll-implement/references/phase-plan.md +23 -0
  119. package/skills/ll-oncall/SKILL.md +3 -2
  120. package/skills/ll-refine/SKILL.md +3 -2
  121. package/skills/ll-research/SKILL.md +3 -2
  122. package/skills/ll-resume/SKILL.md +9 -4
  123. package/skills/ll-update/SKILL.md +6 -1
  124. package/skills/ll-verify/SKILL.md +2 -1
  125. package/skills/ll-verify/references/verifier-briefs.md +3 -0
@@ -0,0 +1,74 @@
1
+ ---
2
+ name: ll-auto
3
+ description: Drives the whole cycle from the state on disk — research, brainstorm, decide, phases, verifications, close — following each stage's own skill in place with the flags it was given, and reports at the end every decision it took alone.
4
+ argument-hint: "[\"<objective>\"] [--research] [--brainstorm] [--interactive] [--auto-decision] [--pause-at <stage|N>] [--from N] [--to N] [--only N] [--verify all] [--redo <stage>] [--dry-run] [--resume]"
5
+ disable-model-invocation: true
6
+ allowed-tools: Bash(${CLAUDE_SKILL_DIR}/scripts/ll-auto.js *)
7
+ ---
8
+
9
+ # Auto
10
+
11
+ Current state: !`${CLAUDE_SKILL_DIR}/scripts/ll-auto.js detect`
12
+
13
+ One invocation, the cycle the repository still owes: the helper reads the state from disk, the flags cut it into a roteiro, and this session walks the roteiro stage by stage, following each stage's own skill as a workflow file. `docs/AUTO.md` carries the run: objective, flags, the roteiro table and one log line per transition, so a compaction or a stop never loses the position. The run starts from the middle when the middle is what exists.
14
+
15
+ Four boundaries hold for the whole run:
16
+ - **The Skill tool is never called.** A stage runs by reading `${CLAUDE_CONFIG_DIR:-$HOME/.claude}/skills/<skill>/SKILL.md` with `Read` (falling back to `skills/<skill>/SKILL.md` in the package tree when this repository is the one running) and following its Flow in place with the stage's arguments. Every skill is locked, this one included; the lock is not worked around, it is honoured by reading the instructions instead of invoking them.
17
+ - **Only this skill follows another skill.** The exception exists because the owner typed the command, and it lasts only for this invocation. No stage started here starts a further `ll-auto`.
18
+ - **No spending ceiling, ever.** Never stop, throttle or shorten a stage to save tokens or money, never announce a budget, never ask whether to continue for cost. The run ends when the roteiro ends, a pause flag says so, or nothing can move without the owner.
19
+ - **No question of its own.** The run never opens `AskUserQuestion`. The only questions the owner sees are the ones a stage asks while running under `--interactive`. When something is missing, the run prints the command that would complete it and stops.
20
+
21
+ Reply to the owner in Portuguese; every file written is in English.
22
+
23
+ ## Deliverables
24
+
25
+ | File | Role | Mutability |
26
+ |---|---|---|
27
+ | `docs/AUTO.md` | the run: objective, flags, roteiro table, decisions taken alone, log | rewritten by this skill on every transition; the roteiro rows only through `auto-md` and the log append-only |
28
+ | `docs/research-*/SUMMARY.md`, `docs/decide/`, `PLAN.md`, `ROADMAP.md`, `PROGRESS.md`, `phases/NN/`, `decisions/`, `BACKLOG.md`, `VERIFICATION.md`, `docs/DELIVERY.md` | whatever each stage writes, by the rules of that stage's own skill | owned by the stage skill; this skill writes none of them directly |
29
+
30
+ ## Flow
31
+
32
+ Helper: `${CLAUDE_SKILL_DIR}/scripts/ll-auto.js`, written `ll-auto.js` below. Read commands exit 0 and answer `{"ok":false,"reason":…}` on a broken input. Commands: `detect`, `roteiro`, `next-cmd`, `report`, `auto-md`.
33
+
34
+ ### 0. State, flags and roteiro
35
+
36
+ `ll-auto.js detect --json` gives the stage table above; `ll-auto.js roteiro --flags "<the arguments>" --objective "<objective>" --json` turns it into the ordered list of stages to run, with a command and a status per entry. `references/stages.md` holds the detection rules, the command per stage and the flag table.
37
+
38
+ Three stops before any work:
39
+ - `needs_objective: true` (no research, no `docs/decide/OPENING.md`, no `PLAN.md`, and no objective in the arguments) — print exactly two lines and stop, with no question: `Nada encontrado neste repositório: sem pesquisa, OPENING.md nem PLAN.md.` and the command to complete, `/ll-auto "<objetivo>" [--research] [--brainstorm]`.
40
+ - `--dry-run` — print the stage table from `detect` (one line per stage, as the helper prints it) and the roteiro table, then stop, before writing `docs/AUTO.md`.
41
+ - an empty roteiro with an objective present — say the cycle has nothing left and hand over.
42
+
43
+ `--resume`, or a plain invocation in a repository that already has `docs/AUTO.md`, reads the flags from that file's `## Flags` section and continues from the first row that is not `done`.
44
+
45
+ ### 1. Open the run
46
+
47
+ Write `docs/AUTO.md` with `ll-auto.js auto-md --objective "<objective>" --flags "<the arguments>"`, redirected to the file. Print the roteiro table to the owner in one screen: this is the plan of the run and the last moment before it starts.
48
+
49
+ ### 2. One stage at a time
50
+
51
+ For every roteiro entry, in order:
52
+ 1. Mark the row `running` in `docs/AUTO.md` and append the log line `<date> <stage> running`.
53
+ 2. Read that stage's `SKILL.md` at the path of boundary 1 and follow its Flow in place, with the entry's command as its arguments. The stage owns its files, its questions and its own ▶ Next line; that line is read, not pasted to the owner, and never starts another skill from here.
54
+ 3. Run `ll-auto.js detect --json` again. Mark the row `done` when the stage's own status turned `done`, `half` when it did not, and append the log line with the evidence the helper gives.
55
+ 4. List the `decisions/*.md` in state `WAITING` and apply `references/run.md`: without `--auto-decision` skip the stages that depend on them and stop when nothing else can run; with `--auto-decision` resolve each to its recommended option and continue.
56
+ 5. When the entry carries `pause_after: true` (`--pause-at`), stop here with `▶ Next — /clear, then ll-auto --resume`.
57
+
58
+ A stage that fails inside its own skill ends the run at that row, marked with its status and its last line in the log; the roteiro below it stays `todo`.
59
+
60
+ ### 3. Close the run
61
+
62
+ `ll-auto.js report --json` lists every `decisions/*.md` carrying `[decided by absence — revisable]`. Print that list as the end-of-run block, one line per file with its title, and write the same block into the `## Decisions taken alone` section of `docs/AUTO.md`. Then the count line and the handover. The end block, verbatim, is in `references/run.md`.
63
+
64
+ ## Completion criterion
65
+
66
+ Every roteiro row is `done`, `skipped` or `waiting` in `docs/AUTO.md`, each with a log line carrying the evidence `detect` gave after the stage; the decisions taken alone are listed both in the conversation and in the file; nothing was decided outside `--auto-decision`; no question was asked by this skill. What the run did not reach is named with the command that would close it.
67
+ The run's last count line, printed and written to `docs/AUTO.md`:
68
+ `stages done X/Y · decisions taken alone N · waiting for the owner K · flags: <the arguments>`
69
+ ▶ Next — /clear, then ll-resume
70
+
71
+ ## References
72
+
73
+ - `references/stages.md` — the detection table, the command per stage and the flag table; step 0.
74
+ - `references/run.md` — the `docs/AUTO.md` template, the WAITING handling and the end-of-run block; steps 1, 2 and 3.
@@ -0,0 +1,75 @@
1
+ # The run — `docs/AUTO.md`, decisions, end block
2
+
3
+ ## The file
4
+
5
+ `ll-auto.js auto-md --objective "<objective>" --flags "<the arguments>"` prints the body; step 1
6
+ redirects it to `docs/AUTO.md`. Five sections, no frontmatter:
7
+
8
+ ```
9
+ # AUTO — autonomous run
10
+
11
+ ## Objective
12
+
13
+ <the objective, on its own line>
14
+
15
+ ## Flags
16
+
17
+ <the flags string, on its own line>
18
+
19
+ ## Roteiro
20
+
21
+ | # | stage | command | status | evidence |
22
+ |---|---|---|---|---|
23
+ | 1 | phase-08 | ll-implement 08 --no-talk | todo | ROADMAP row: PLANNED |
24
+
25
+ ## Decisions taken alone
26
+
27
+ (filled at the end of the run)
28
+
29
+ ## Log
30
+
31
+ (one dated line per stage transition)
32
+ ```
33
+
34
+ Status of a row: `todo` · `half` · `running` · `done` · `skipped` · `waiting`. Exactly one row is
35
+ `running` at a time. The `evidence` column is the one `detect` gave for that stage, refreshed when
36
+ the row is marked `done` or `half`.
37
+
38
+ A log line is `<YYYY-MM-DD HH:MM> <stage> <status> — <evidence or the stage's last line>`, appended,
39
+ never rewritten. The `## Flags` section is what `--resume` reads, so it is written once and left
40
+ alone; a run restarted with different flags rewrites the whole file.
41
+
42
+ ## Decisions only the owner can take
43
+
44
+ After every stage, list the `decisions/*.md` whose status is `WAITING`.
45
+
46
+ Without `--auto-decision`:
47
+ - the run continues through the stages that do not depend on them; a phase whose ROADMAP section
48
+ carries `stop: owner` is skipped and its row is marked `skipped`;
49
+ - the rows that depend on a WAITING decision are marked `waiting`;
50
+ - when nothing else can run, print the WAITING list — one line per file, with the question and the
51
+ recommended option — and stop with `▶ Next — /clear, then ll-auto --resume`.
52
+
53
+ With `--auto-decision`:
54
+ - append to each WAITING decision file a dated line
55
+ `status: DECIDED — <the recommended option> [decided by absence — revisable]`;
56
+ - clear the `stop: owner` marks those decisions held in ROADMAP;
57
+ - continue the roteiro; the marker is what the end block lists.
58
+
59
+ Nothing is decided outside these two paths: a decision that has no recommended option stays
60
+ `WAITING` and stops the run even under `--auto-decision`, because there is nothing to pick.
61
+
62
+ ## The end block
63
+
64
+ `ll-auto.js report --json` answers `{"decisions":[{file, title, line}]}` for every `decisions/*.md`
65
+ carrying `[decided by absence — revisable]`. Print it, and write the same lines into the
66
+ `## Decisions taken alone` section:
67
+
68
+ ```
69
+ Decisions taken alone — review them:
70
+ - decisions/DEC-0007-retry-policy.md — retry policy for the provider adapter (line 12)
71
+ - decisions/DEC-0009-report-window.md — the window the report counts (line 9)
72
+ ```
73
+
74
+ An empty list is printed as `Decisions taken alone: none`. Then the count line of the Completion
75
+ criterion, then the handover. The run ends there: the next command belongs to the owner.
@@ -0,0 +1,66 @@
1
+ # Stages — detection, commands, flags
2
+
3
+ ## Detection
4
+
5
+ `ll-auto.js detect` reads only the files below and is deterministic: the same tree gives the same
6
+ table twice. Every entry answers `{id, status, evidence}` with status `todo`, `half` or `done`.
7
+
8
+ | stage | done when | half when | evidence field |
9
+ |---|---|---|---|
10
+ | `research` | a `docs/research-*/SUMMARY.md` exists | never | the path, or what is missing |
11
+ | `brainstorm` | `docs/decide/OPENING.md` or any `phases/NN/DECISIONS.md` exists | never | the path, or what is missing |
12
+ | `decide` | `PLAN.md` at the root and `PROGRESS.md` with an `ll-state` block | never | `PLAN.md + PROGRESS.md ll-state block` |
13
+ | `phase-NN` | the ROADMAP row state starts with `DONE`, or `## Epilogue — phase NN` exists over a board that is for another phase or all `passes: true` | `phases/NN/PLAN.md` exists and the phase is not done | the ROADMAP row, or the epilogue and the milestones still red |
14
+ | `verify-NN` | `phases/NN/VERIFICATION.md` exists | never | the path, or what is missing |
15
+ | `close` | `docs/DELIVERY.md` exists and the last ROADMAP phase has an epilogue | never | the path, or what is missing |
16
+
17
+ One `phase-NN` per row of the ROADMAP table; with no ROADMAP.md, the phases come from the same
18
+ table inline at `PLAN.md` §8, same columns (`| phase | name | depends_on | requirements | state |`);
19
+ with no `PLAN.md`, none. One `verify-NN` per phase row. `close` is always last.
20
+
21
+ A phase with an epilogue over a board that still shows a milestone `passes: false` is `half`, not
22
+ `done`: the epilogue was written, the phase was not finished. `ll-implement NN` resumes it from the
23
+ board, which is why a `half` phase enters the roteiro with the same command as a `todo` one.
24
+
25
+ ## The command per stage
26
+
27
+ | stage | command | note |
28
+ |---|---|---|
29
+ | `research` | `ll-research "<objective>"` | only with `--research`; the objective is the run's |
30
+ | `brainstorm` | `ll-brainstorm project --no-talk` | only with `--brainstorm`; `--interactive` drops `--no-talk` |
31
+ | `decide` | `ll-decide project --no-talk` | enters whenever it is `todo` |
32
+ | `phase-NN` | `ll-implement NN --no-talk` | `--interactive` drops `--no-talk`; a `half` phase resumes with the same command |
33
+ | `verify-NN` | `ll-verify NN` | never `--external`: that flag hands the audit to another session, which this run is not |
34
+ | `close` | `ll-close --no-talk` | `--milestone <name>` when the ROADMAP is complete |
35
+
36
+ The number is written as the ROADMAP writes it (`08`, not `8`). The roteiro entry carries the
37
+ command ready to follow; the stage's own skill decides everything inside it.
38
+
39
+ ## Which stages enter the roteiro
40
+
41
+ `research` and `brainstorm` enter only when their flag is set and their status is not `done`.
42
+ `decide` enters when it is `todo`. Every phase row that is not `done` enters, filtered by
43
+ `--from/--to/--only`, the `half` ones first as a resume. `verify-NN` enters right after its phase
44
+ when `--verify all` is set or when the phase's epilogue names `ll-verify NN` in its handover line.
45
+ `close` enters last, unless `--only` or `--to` cut the roteiro before the last row.
46
+
47
+ ## Flags
48
+
49
+ | flag | effect |
50
+ |---|---|
51
+ | `"<objective>"` | the run's objective: the argument of `ll-research`, and the `## Objective` of `docs/AUTO.md` |
52
+ | `--research` | put `research` in the roteiro when it is not done |
53
+ | `--brainstorm` | put `brainstorm` in the roteiro when it is not done |
54
+ | `--interactive` | drop `--no-talk` from `ll-brainstorm` and `ll-implement`: those stages talk to the owner |
55
+ | `--auto-decision` | resolve every owner-only decision to its recommended option and continue; without it the run stops when one blocks the way |
56
+ | `--pause-at <stage\|N>` | stop after that stage or phase, with `▶ Next — /clear, then ll-auto --resume` |
57
+ | `--from N` | skip the phase rows below N |
58
+ | `--to N` | skip the phase rows above N |
59
+ | `--only N` | phase N alone; implies `--from N --to N` and cuts `close` |
60
+ | `--verify all` | a `verify-NN` after every phase, whatever the epilogue says |
61
+ | `--redo <stage>` | force a `done` stage back to `todo` in the roteiro; repeatable |
62
+ | `--dry-run` | print the roteiro table and stop, before writing `docs/AUTO.md` |
63
+ | `--resume` | read the flags from the `## Flags` section of `docs/AUTO.md` and continue from the first row that is not `done` |
64
+
65
+ `--pause-at` and `--redo` are repeatable. A plain invocation in a repository that already carries
66
+ `docs/AUTO.md` behaves as `--resume`: the flags of the file are the flags of the run.
@@ -0,0 +1,345 @@
1
+ #!/usr/bin/env node
2
+ // ll-auto helper — reads the cycle state from disk. Self-contained: `fs` and `path` only.
3
+ // Read commands never fail: on a broken input they print {"ok":false,"reason":…} and exit 0.
4
+ 'use strict';
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const J = path.join;
8
+ function readText(f) { try { return fs.readFileSync(f, 'utf8'); } catch { return null; } }
9
+ function isDir(p) { try { return fs.statSync(p).isDirectory(); } catch { return false; } }
10
+ function exists(p) { try { fs.statSync(p); return true; } catch { return false; } }
11
+ function ls(d) { try { return fs.readdirSync(d).sort(); } catch { return []; } }
12
+ function nn(v) { return String(v).trim().padStart(2, '0'); }
13
+
14
+ // --- state block reader, copied from scripts/ll-tools.js (<ll-shared:state>) -----------------
15
+ function readAnchoredBlock(file, name) {
16
+ let txt;
17
+ try { txt = fs.readFileSync(file, 'utf8'); } catch { return null; }
18
+ const all = txt.split('\n');
19
+ const open = new RegExp('^<!--\\s*' + name + '\\s*-->\\s*$');
20
+ const close = new RegExp('^<!--\\s*/' + name + '\\s*-->\\s*$');
21
+ let start = -1, end = -1;
22
+ for (let i = 0; i < all.length; i++) {
23
+ if (start < 0) { if (open.test(all[i])) start = i; } else if (close.test(all[i])) { end = i; break; }
24
+ }
25
+ if (start < 0 || end < 0) return null;
26
+ return { file, start, end, all, lines: all.slice(start + 1, end) };
27
+ }
28
+ function llSplitFlow(str) { // top-level commas only
29
+ const out = [];
30
+ let cur = '', depth = 0, q = null;
31
+ for (const ch of str) {
32
+ if (q) { cur += ch; if (ch === q) q = null; continue; }
33
+ if (ch === '"' || ch === "'") { q = ch; cur += ch; continue; }
34
+ if (ch === '[' || ch === '{') depth++;
35
+ else if (ch === ']' || ch === '}') depth--;
36
+ else if (ch === ',' && depth === 0) { out.push(cur); cur = ''; continue; }
37
+ cur += ch;
38
+ }
39
+ if (cur.trim() !== '') out.push(cur);
40
+ return out;
41
+ }
42
+ function llScalar(raw) {
43
+ const s = String(raw).trim();
44
+ if (/^\[[\s\S]*\]$/.test(s)) return llSplitFlow(s.slice(1, -1)).map(llScalar);
45
+ if (/^\{[\s\S]*\}$/.test(s)) {
46
+ const o = {};
47
+ for (const part of llSplitFlow(s.slice(1, -1))) {
48
+ const m = /^\s*([A-Za-z_][\w-]*)\s*:\s*([\s\S]*)$/.exec(part);
49
+ if (m) o[m[1]] = llScalar(m[2]);
50
+ }
51
+ return o;
52
+ }
53
+ if (/^(true|yes)$/i.test(s)) return true;
54
+ if (/^(false|no)$/i.test(s)) return false;
55
+ if (/^".*"$/.test(s) || /^'.*'$/.test(s)) return s.slice(1, -1);
56
+ return s;
57
+ }
58
+ function parseStateBlockLite(bl) {
59
+ const st = { phase: null, milestones: {}, order: [] };
60
+ let inM = false;
61
+ for (let i = 0; i < bl.length; i++) {
62
+ const line = bl[i].replace(/\s+$/, '');
63
+ if (!line.trim() || /^\s*#/.test(line)) continue;
64
+ let m = /^phase:\s*(.*)$/.exec(line);
65
+ if (m) { st.phase = String(llScalar(m[1])); inM = false; continue; }
66
+ if (/^milestones:\s*(\{\s*\})?$/.test(line)) { inM = true; continue; }
67
+ m = /^(\s*)(M\d+|G-\d+):\s*\{(.*)\}\s*$/.exec(line);
68
+ if (m && inM) {
69
+ st.milestones[m[2]] = llScalar('{' + m[3] + '}');
70
+ st.order.push({ id: m[2], index: i, indent: m[1] });
71
+ } else if (/^\S/.test(line)) inM = false;
72
+ }
73
+ return st;
74
+ }
75
+
76
+ // --- phase rows ------------------------------------------------------------------------------
77
+ // Rows of the ROADMAP phase table: a header carrying `phase` and numeric first cells.
78
+ function phaseRows(md) {
79
+ const rows = [], seen = new Set();
80
+ let cols = null;
81
+ for (const line of String(md || '').split('\n')) {
82
+ if (!/^\s*\|/.test(line)) { cols = null; continue; }
83
+ const cells = line.trim().replace(/^\|/, '').replace(/\|$/, '').split('|').map((c) => c.trim());
84
+ if (/^:?-{2,}/.test(cells[0] || '')) continue; // separator row
85
+ if (!/^\d{1,3}$/.test(cells[0] || '')) { cols = cells.map((c) => c.toLowerCase()); continue; }
86
+ if (!cols || cols.indexOf('phase') < 0) continue;
87
+ const i = cols.indexOf('state');
88
+ const id = nn(cells[0]);
89
+ if (seen.has(id)) continue;
90
+ seen.add(id);
91
+ rows.push({ nn: id, state: (i >= 0 ? cells[i] : cells[cells.length - 1]) || '' });
92
+ }
93
+ return rows;
94
+ }
95
+ // Without ROADMAP.md the rows live in the `## §8 Phases` table of PLAN.md (`| NN | name | … |`,
96
+ // a header cell `phase` and, when there is one, a `state` cell); without PLAN.md there are no
97
+ // phases at all. The §8 slice runs to the next `## ` heading — `$` under /m ends at every line.
98
+ function rowsOf(root) {
99
+ const rm = readText(J(root, 'ROADMAP.md'));
100
+ if (rm !== null) return phaseRows(rm);
101
+ const plan = readText(J(root, 'PLAN.md'));
102
+ if (plan === null) return [];
103
+ const h = /^##\s*§?8\b[^\n]*\n/m.exec(plan);
104
+ if (!h) return [];
105
+ const rest = plan.slice(h.index + h[0].length);
106
+ const next = rest.search(/^##\s/m);
107
+ return phaseRows(next < 0 ? rest : rest.slice(0, next));
108
+ }
109
+
110
+ // --- detect ----------------------------------------------------------------------------------
111
+ function phaseStage(root, row, ctx) {
112
+ if (/^DONE/.test(row.state)) return { status: 'done', evidence: 'ROADMAP row: ' + row.state };
113
+ const plan = 'phases/' + row.nn + '/PLAN.md';
114
+ if (ctx.epilogues.has(row.nn)) {
115
+ const own = ctx.state && nn(ctx.state.phase || '') === row.nn;
116
+ const red = own ? Object.keys(ctx.state.milestones).filter((k) => ctx.state.milestones[k].passes !== true) : [];
117
+ if (!own || red.length === 0) return { status: 'done', evidence: 'PROGRESS.md: ## Epilogue — phase ' + row.nn };
118
+ if (exists(J(root, 'phases', row.nn, 'PLAN.md'))) {
119
+ return { status: 'half', evidence: 'epilogue over a board with ' + red.join(', ') + ' not passing' };
120
+ }
121
+ }
122
+ if (exists(J(root, 'phases', row.nn, 'PLAN.md'))) return { status: 'half', evidence: plan };
123
+ return { status: 'todo', evidence: 'ROADMAP row: ' + (row.state || '-') };
124
+ }
125
+
126
+ function detect(root) {
127
+ const stages = [];
128
+ const add = (id, status, evidence) => stages.push({ id, status, evidence });
129
+ const research = ls(J(root, 'docs')).filter((n) => /^research-/.test(n) && exists(J(root, 'docs', n, 'SUMMARY.md')));
130
+ add('research', research.length ? 'done' : 'todo',
131
+ research.length ? 'docs/' + research[0] + '/SUMMARY.md' : 'no docs/research-*/SUMMARY.md');
132
+
133
+ const opening = exists(J(root, 'docs', 'decide', 'OPENING.md'));
134
+ const decided = ls(J(root, 'phases')).filter((n) => exists(J(root, 'phases', n, 'DECISIONS.md')));
135
+ add('brainstorm', opening || decided.length ? 'done' : 'todo',
136
+ opening ? 'docs/decide/OPENING.md'
137
+ : decided.length ? 'phases/' + decided[0] + '/DECISIONS.md' : 'no docs/decide/OPENING.md');
138
+
139
+ const progress = J(root, 'PROGRESS.md');
140
+ const block = readAnchoredBlock(progress, 'll-state');
141
+ const state = block ? parseStateBlockLite(block.lines) : null;
142
+ const hasPlan = exists(J(root, 'PLAN.md'));
143
+ add('decide', hasPlan && state ? 'done' : 'todo',
144
+ hasPlan && state ? 'PLAN.md + PROGRESS.md ll-state block'
145
+ : hasPlan ? 'PROGRESS.md carries no ll-state block' : 'no PLAN.md at the root');
146
+ const epilogues = new Set();
147
+ for (const line of (readText(progress) || '').split('\n')) {
148
+ const h = /^## Epilogue — phase (\S+)/.exec(line);
149
+ if (h) epilogues.add(nn(h[1]));
150
+ }
151
+ const rows = rowsOf(root);
152
+ const ctx = { epilogues, state };
153
+ for (const row of rows) {
154
+ const s = phaseStage(root, row, ctx);
155
+ add('phase-' + row.nn, s.status, s.evidence);
156
+ }
157
+ for (const row of rows) {
158
+ const v = 'phases/' + row.nn + '/VERIFICATION.md';
159
+ const ok = exists(J(root, v));
160
+ add('verify-' + row.nn, ok ? 'done' : 'todo', ok ? v : 'no ' + v);
161
+ }
162
+ const last = rows.length ? rows[rows.length - 1].nn : null;
163
+ const delivery = exists(J(root, 'docs', 'DELIVERY.md'));
164
+ const closed = delivery && last !== null && epilogues.has(last);
165
+ add('close', closed ? 'done' : 'todo',
166
+ closed ? 'docs/DELIVERY.md + epilogue for phase ' + last
167
+ : delivery ? 'docs/DELIVERY.md without an epilogue for the last phase' : 'no docs/DELIVERY.md');
168
+ return { ok: true, root, stages };
169
+ }
170
+
171
+ // --- next-cmd --------------------------------------------------------------------------------
172
+ // The handoff grammar of lint-contract rule 6: `▶ Next — /clear, then <cmd> (alternatives)`.
173
+ // Backticks are decoration; one trailing parenthetical is dropped. The last line in the file wins.
174
+ function nextCmdOf(txt) {
175
+ let out = '';
176
+ for (const line of String(txt || '').split('\n')) {
177
+ const m = /▶ Next\s*—(.*)$/.exec(line);
178
+ if (!m) continue;
179
+ const hand = m[1].replace(/`/g, '').trim();
180
+ const o = /^\/clear\s*,\s*then\s+(.*)$/.exec(hand);
181
+ if (o) out = o[1].replace(/\([^()]*\)\s*$/, '').trim();
182
+ }
183
+ return out;
184
+ }
185
+ // The slice of PROGRESS.md owned by phase NN's epilogue; the whole file when it has none.
186
+ function epilogueText(root, id) {
187
+ const txt = readText(J(root, 'PROGRESS.md')) || '';
188
+ const lines = txt.split('\n');
189
+ let start = -1;
190
+ for (let i = 0; i < lines.length; i++) {
191
+ const h = /^## Epilogue — phase (\S+)/.exec(lines[i]);
192
+ if (h && nn(h[1]) === id) start = i;
193
+ }
194
+ if (start < 0) return txt;
195
+ let end = lines.length;
196
+ for (let i = start + 1; i < lines.length; i++) if (/^## /.test(lines[i])) { end = i; break; }
197
+ return lines.slice(start, end).join('\n');
198
+ }
199
+
200
+ // --- roteiro ---------------------------------------------------------------------------------
201
+ const FLAG_ONE = { '--research': 'research', '--brainstorm': 'brainstorm', '--interactive': 'interactive',
202
+ '--auto-decision': 'auto_decision', '--dry-run': 'dry_run', '--resume': 'resume' };
203
+ const FLAG_VAL = { '--pause-at': 'pause_at', '--redo': 'redo', '--from': 'from', '--to': 'to',
204
+ '--only': 'only', '--verify': 'verify' };
205
+ // A stage reference in --pause-at / --redo: an id, a bare phase number, or `phase 7`.
206
+ function stageId(x) {
207
+ const s = String(x || '').trim();
208
+ if (/^\d{1,3}$/.test(s)) return 'phase-' + nn(s);
209
+ const m = /^(phase|verify)[\s-]*(\d{1,3})$/.exec(s);
210
+ return m ? m[1] + '-' + nn(m[2]) : s;
211
+ }
212
+ function parseFlags(str) {
213
+ const f = { pause_at: [], redo: [], from: null, to: null, only: null, verify: null };
214
+ for (const k of Object.values(FLAG_ONE)) f[k] = false;
215
+ const t = String(str || '').trim().split(/\s+/).filter(Boolean);
216
+ for (let i = 0; i < t.length; i++) {
217
+ if (FLAG_ONE[t[i]]) { f[FLAG_ONE[t[i]]] = true; continue; }
218
+ const k = FLAG_VAL[t[i]];
219
+ if (!k) continue;
220
+ const v = i + 1 < t.length && !/^--/.test(t[i + 1]) ? t[++i] : null;
221
+ if (k === 'pause_at' || k === 'redo') { if (v !== null) f[k].push(v); } else f[k] = v;
222
+ }
223
+ if (f.only !== null) { f.from = f.only; f.to = f.only; } // --only N implies from = to = N
224
+ return f;
225
+ }
226
+ const num = (x) => Number(String(x).replace(/[^\d.]/g, '')) || 0;
227
+ const talk = (interactive) => (interactive ? '' : ' --no-talk'); // --interactive drops --no-talk
228
+
229
+ function roteiro(root, flagStr, objective) {
230
+ const d = detect(root);
231
+ const f = parseFlags(flagStr);
232
+ const st = {};
233
+ for (const s of d.stages) st[s.id] = s;
234
+ const redo = new Set(f.redo.map(stageId));
235
+ const pause = new Set(f.pause_at.map(stageId));
236
+ const obj = String(objective || '').trim();
237
+ // Empty repository per D-03-06: no research, no OPENING.md, no PLAN.md.
238
+ const bare = st.research.status === 'todo' && st.brainstorm.status === 'todo' && !exists(J(root, 'PLAN.md'));
239
+ if (bare && !obj) return { ok: true, root, needs_objective: true, roteiro: [] };
240
+ const out = [];
241
+ const want = (id) => redo.has(id) || !st[id] || st[id].status !== 'done';
242
+ const push = (id, command) => out.push({ stage: id, command,
243
+ status: st[id] ? st[id].status : 'todo', pause_after: pause.has(id) });
244
+ if (f.research && want('research')) push('research', 'll-research "' + (obj || '<objective>') + '"');
245
+ if (f.brainstorm && want('brainstorm')) push('brainstorm', 'll-brainstorm project' + talk(f.interactive));
246
+ if (want('decide')) push('decide', 'll-decide project --no-talk');
247
+ const rows = rowsOf(root);
248
+ const pend = rows.filter((r) => want('phase-' + r.nn));
249
+ const inRange = (r) => (f.from === null || num(r.nn) >= num(f.from))
250
+ && (f.to === null || num(r.nn) <= num(f.to));
251
+ const kept = pend.filter(inRange);
252
+ const half = (r) => st['phase-' + r.nn].status === 'half';
253
+ for (const r of kept.filter(half).concat(kept.filter((r) => !half(r)))) { // a half phase resumes first
254
+ push('phase-' + r.nn, 'll-implement ' + r.nn + talk(f.interactive));
255
+ const asked = new RegExp('^ll-verify\\s+0*' + num(r.nn) + '$').test(nextCmdOf(epilogueText(root, r.nn)));
256
+ if ((f.verify === 'all' || asked) && want('verify-' + r.nn)) push('verify-' + r.nn, 'll-verify ' + r.nn);
257
+ }
258
+ // Close only when the roteiro covers every phase still open: --only, --from or --to cut it out.
259
+ if (f.only === null && kept.length === pend.length && want('close')) push('close', 'll-close --no-talk');
260
+ return { ok: true, root, needs_objective: false, roteiro: out };
261
+ }
262
+
263
+ // --- report ----------------------------------------------------------------------------------
264
+ const MARK = '[decided by absence — revisable]';
265
+ function report(root) {
266
+ const dir = J(root, 'decisions'), out = [];
267
+ for (const name of ls(dir)) {
268
+ if (!/\.md$/.test(name)) continue;
269
+ const lines = (readText(J(dir, name)) || '').split('\n');
270
+ const i = lines.findIndex((l) => l.indexOf(MARK) >= 0);
271
+ if (i < 0) continue;
272
+ const h = lines.find((l) => /^#\s+/.test(l)) || '';
273
+ out.push({ file: 'decisions/' + name, title: h.replace(/^#\s+/, '').trim(), line: i + 1 });
274
+ }
275
+ return { ok: true, root, decisions: out };
276
+ }
277
+
278
+ // --- auto-md ---------------------------------------------------------------------------------
279
+ // The docs/AUTO.md body of D-03-07: objective, flags, the roteiro table, then two empty sections.
280
+ function autoMd(root, objective, flagStr) {
281
+ const ev = {};
282
+ for (const s of detect(root).stages) ev[s.id] = s.evidence;
283
+ const r = roteiro(root, flagStr, objective);
284
+ const L = ['# AUTO — autonomous run', '',
285
+ '## Objective', '', String(objective || '').trim() || '<objective>', '',
286
+ '## Flags', '', String(flagStr || '').trim() || '(none)', '',
287
+ '## Roteiro', '', '| # | stage | command | status | evidence |', '|---|---|---|---|---|'];
288
+ r.roteiro.forEach((e, i) => L.push('| ' + (i + 1) + ' | ' + e.stage + ' | ' + e.command + ' | '
289
+ + e.status + ' | ' + (ev[e.stage] || '—') + ' |'));
290
+ if (!r.roteiro.length) {
291
+ L.push('| — | — | — | — | ' + (r.needs_objective ? 'no objective given' : 'nothing left to run') + ' |');
292
+ }
293
+ L.push('', '## Decisions taken alone', '', '(filled at the end of the run)', '',
294
+ '## Log', '', '(one dated line per stage transition)');
295
+ return { ok: true, root, markdown: L.join('\n') };
296
+ }
297
+
298
+ // --- cli -------------------------------------------------------------------------------------
299
+ function parseArgv(argv) {
300
+ const flags = {}, pos = [];
301
+ for (let i = 0; i < argv.length; i++) {
302
+ const a = argv[i];
303
+ const kv = /^--([^=]+)=([\s\S]*)$/.exec(a);
304
+ if (kv) { flags[kv[1]] = kv[2]; continue; }
305
+ if (a === '--cwd' || a === '--flags' || a === '--objective') { flags[a.slice(2)] = argv[++i]; continue; }
306
+ if (/^--/.test(a)) { flags[a.slice(2)] = true; continue; }
307
+ pos.push(a);
308
+ }
309
+ return { flags, pos };
310
+ }
311
+ function rootOf(a) {
312
+ const r = path.resolve(String(a.flags.cwd || process.cwd()));
313
+ if (!isDir(r)) throw new Error('not a directory: ' + r);
314
+ return r;
315
+ }
316
+ const C = {
317
+ detect: (a) => detect(rootOf(a)),
318
+ roteiro: (a) => roteiro(rootOf(a), a.flags.flags, a.flags.objective),
319
+ 'next-cmd': (a) => ({ ok: true, command: nextCmdOf(readText(path.resolve(a.pos[0] || ''))) }),
320
+ report: (a) => report(rootOf(a)),
321
+ 'auto-md': (a) => autoMd(rootOf(a), a.flags.objective, a.flags.flags),
322
+ };
323
+ const FMT = {
324
+ detect: (r) => r.stages.map((s) => s.id.padEnd(12) + ' ' + s.status.padEnd(5) + ' ' + s.evidence).join('\n'),
325
+ roteiro: (r) => (r.needs_objective ? 'needs_objective'
326
+ : r.roteiro.map((e, i) => (i + 1) + '. ' + e.stage.padEnd(12) + ' ' + e.command
327
+ + (e.pause_after ? ' [pause]' : '')).join('\n')),
328
+ 'next-cmd': (r) => r.command,
329
+ report: (r) => r.decisions.map((d) => d.file + ':' + d.line + ' ' + d.title).join('\n'),
330
+ 'auto-md': (r) => r.markdown,
331
+ };
332
+
333
+ function main() {
334
+ const argv = process.argv.slice(2), cmd = argv[0];
335
+ try {
336
+ if (!C[cmd]) throw new Error('usage: ll-auto.js <' + Object.keys(C).join('|') + '> [args] [--cwd <dir>] [--json]');
337
+ const a = parseArgv(argv.slice(1));
338
+ const res = C[cmd](a);
339
+ process.stdout.write((a.flags.json || !FMT[cmd] ? JSON.stringify(res) : FMT[cmd](res)) + '\n');
340
+ } catch (e) {
341
+ process.stdout.write(JSON.stringify({ ok: false, reason: (e && e.message) || String(e) }) + '\n');
342
+ }
343
+ process.exit(0);
344
+ }
345
+ main();