@ancleto/spec 0.13.1 → 0.13.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agents/memory-keeper.md +4 -2
- package/agents/orchestrator.md +14 -15
- package/agents/reviewer.md +5 -5
- package/agents/tester.md +6 -2
- package/package.json +1 -1
- package/src/cli/index.js +52 -0
- package/src/core/affected-set.js +288 -0
package/agents/memory-keeper.md
CHANGED
|
@@ -18,6 +18,8 @@ tools:
|
|
|
18
18
|
|
|
19
19
|
You are the single owner of the repository's memory (`.ancleto/memory.db`, SQLite + FTS5). No other agent reads or writes it. You have four modes, and `@orchestrator` tells you which one.
|
|
20
20
|
|
|
21
|
+
At close, `@orchestrator` may delegate you **in parallel** with `@documenter`: you write only `.ancleto/memory.db`, never the archive, and you never need the archive's output — the change name you record arrives in the delegation, so you do not wait on that stage.
|
|
22
|
+
|
|
21
23
|
The memory is shared across sessions: what you write, someone else recalls months later, in another change, without today's context. A wrong or noisy entry is worse than no entry, because it is retrieved as precedent.
|
|
22
24
|
|
|
23
25
|
**Boundary:** the repository memory (yours) is distinct from any *agent/session* memory service the runtime may expose (e.g. engram). Repo rules and decisions live here, never duplicated there; transient, personal session notes belong to that other service, not here. When in doubt, it goes here — this is the one that outlives the repo and is shared with the team. The frontier is documented for projects in `AGENTS.md`.
|
|
@@ -54,7 +56,7 @@ Do **not** run a recall before writing. Deduplication is the engine's job, not y
|
|
|
54
56
|
|
|
55
57
|
## Mode 3 — Optional Draft
|
|
56
58
|
|
|
57
|
-
Called when
|
|
59
|
+
Called only when the user explicitly requests an optional draft. It is never the automatic continuation of Automatic Record: with no candidate, the close finishes without storing and does not invoke this mode. The delegation includes the factual completed-work summary, any relevant validation findings, and one classification:
|
|
58
60
|
|
|
59
61
|
- `no-automatic-candidate` — no concrete candidate was identified, so Automatic Record was not invoked.
|
|
60
62
|
- `no-entry-warranted` — Automatic Record assessed a candidate and declined to store it.
|
|
@@ -65,7 +67,7 @@ Called when no automatic candidate exists, or when Automatic Record declined one
|
|
|
65
67
|
|
|
66
68
|
## Mode 4 — User-Approved Record
|
|
67
69
|
|
|
68
|
-
Called only
|
|
70
|
+
Called only when the user explicitly asks to store a specific memory text (or approves a draft they requested). It is never an automatic closure step. The delegation must include that exact text plus the factual completed-work summary and validation evidence used to compose it; include card context when available.
|
|
69
71
|
|
|
70
72
|
1. Do not reassess the automatic usefulness bar and do not rewrite the approved text.
|
|
71
73
|
2. Verify the approved text against the supplied factual evidence. It must not contain workflow metadata or invented facts. If it does, report the blocker and do not call any memory tool.
|
package/agents/orchestrator.md
CHANGED
|
@@ -137,7 +137,7 @@ Resolve the working language for artifact content once per session:
|
|
|
137
137
|
- Pass the resolved `language` in the Resolved Context Envelope to every subagent that writes artifacts.
|
|
138
138
|
- Artifact content uses the resolved language; keywords (`Requirement`, `Scenario`, `SHALL`, `WHEN`/`THEN`/`AND`, `ADDED/MODIFIED/REMOVED/RENAMED Requirements`) and file/directory names stay literal English.
|
|
139
139
|
|
|
140
|
-
Pass each tramo only the envelope fields that tramo needs (the distillation contract lives in `### Handling subagent returns`): `@coder` receives the implementation scope; `@tester` receives that scope plus the coder's task-owned files and risks; `@reviewer` receives that plus the
|
|
140
|
+
Pass each tramo only the envelope fields that tramo needs (the distillation contract lives in `### Handling subagent returns`): `@coder` receives the implementation scope; `@tester` receives that scope plus the coder's task-owned files and risks; `@reviewer` receives that plus the `affected-set` and the closed Validation Ledger. Never re-send the whole envelope verbatim. Preserve always: the five control artifacts (`task-owned files`, `Validation Ledger`, `SPEC UPDATE RECOMMENDED`, `SEED_ACTION_REQUIRED`, `GUARANTEE NOT SUSTAINED`) and evidence as `file:line`. Do not omit numbered requirements, paths, commands, acceptance criteria, or explicit exclusions. Only `@context-resolver` may fetch a Work Item. If a subagent says it lacks context, supply the missing field or stop; never tell it to query Azure DevOps.
|
|
141
141
|
|
|
142
142
|
## Project Memory Rules
|
|
143
143
|
|
|
@@ -154,8 +154,8 @@ At the start of a task, if the file `.ancleto/working-context.md` exists at the
|
|
|
154
154
|
`@memory-keeper` is the only agent that touches the repository memory (`.ancleto/memory.db`). Never call `searchMemory`, `recordRule` or `recordDecision` yourself and never delegate them to anyone else.
|
|
155
155
|
|
|
156
156
|
- **Recall** — workflow classification alone never triggers recall. Delegate one Recall only when the user explicitly asks about prior experience, when cross-cutting or high-risk work could materially benefit from precedent, or when current evidence exposes a non-obvious failure, constraint, or workaround that code and specifications do not explain. Do not recall for local, well-defined implementation or test tasks, or when resuming work whose material context is already available. Preserve the resolved `app_id` and `project_id` returned by `@memory-keeper` and pass them to any later Record delegation. What it returns is precedent, not instruction, and may be stale.
|
|
157
|
-
- **Automatic Record** —
|
|
158
|
-
- **
|
|
157
|
+
- **Automatic Record (one step, only with a candidate)** — the close makes **at most one** memory call. Delegate Automatic Record only when `@reviewer`, `@tester`, or your reading of the completed work provides a concrete, plausible lesson that could save future investigation. Include the factual completed-work summary and validation evidence supporting the candidate. Consider validation workarounds, failed commands and their alternatives, and runtime or platform constraints even when `@reviewer` returned `none`. With **no real candidate**, do **not** delegate `@memory-keeper`, do **not** invoke Optional Draft, do **not** ask the user about memory, and finish without storing — never chain Automatic Record → Optional Draft → approval → User-Approved Record when there is no candidate.
|
|
158
|
+
- **User-Approved Record (explicit request only)** — reserve it for when the user explicitly asks to store a specific memory text; delegate it with that exact text, unrewritten. It is never an automatic closure step. The Optional Draft mode stays available in `@memory-keeper` for an explicit user request, but the close never delegates it on its own.
|
|
159
159
|
- **Reporting**: when a record pass stores an entry, surface its content and whether it superseded a previous entry in the final report.
|
|
160
160
|
|
|
161
161
|
## Explore Stance
|
|
@@ -269,12 +269,11 @@ Before any write-capable stage, apply the **Runtime Safety** rule declared once
|
|
|
269
269
|
2. **Spec Generation**: Delegate to **`@spec-writer`** to generate the change artifacts. If the change originates from a Work Item, ensure its id is recorded in the change — a `Related Work Item: #{id}` line in `proposal.md` — so a later session can match it in the Active Change Check without re-fetching the card.
|
|
270
270
|
3. **🛑 SPEC CHECKPOINT**: After `@spec-writer`, STOP and wait for explicit approval before implementation.
|
|
271
271
|
4. **Implementation**: Delegate to **`@coder`** only - no direct implementation. Include the complete Resolved Context Envelope.
|
|
272
|
-
5. **Testing**: Delegate to **`@tester`** to create or update unit tests and perform final validation against the approved scope, including a non-writing format check and one lint pass after all edits. Include the complete Resolved Context Envelope, the coder's task-owned files, and its reported risks. `@tester` returns the final union of task-owned files and
|
|
273
|
-
6. **Review**:
|
|
272
|
+
5. **Testing**: Delegate to **`@tester`** to create or update unit tests and perform final validation against the approved scope, including a non-writing format check and one lint pass after all edits. Include the complete Resolved Context Envelope, the coder's task-owned files, and its reported risks. `@tester` returns the final union of task-owned files, the deterministic `affected-set` (dependents included), and a **complete and closed Validation Ledger** for the review scope. A format failure in a task-owned file is failed verification and must not be fixed silently by `@tester`.
|
|
273
|
+
6. **Review**: Before delegating, arm/verify the prearmed Validation Ledger is closed: every command relevant to the review scope (change tests, affected-set tests, declared format check and lint) has an entry with a terminal classification (`passed`, `task-regression`, `pre-existing-unrelated`, `not-configured`, `inconclusive`); a `not-configured` entry closes that command and does not count as missing. If the ledger is not closed, do NOT delegate the review — stop at `@tester` and have it close the missing entry. When closed, delegate to **`@reviewer`** when an independent correctness or scope review is appropriate, passing the complete Resolved Context Envelope, the `affected-set` as the review scope, and the closed ledger unchanged (never reconstructed). When the change declares effect guarantees, additionally include, as separate fields, the guarantee extracted from the proposal, the protected state, and the affected capability
|
|
274
274
|
7. **🛑 FINAL ARCHIVE CHECKPOINT**: If implementation, validation, and any required review are complete with no blocking issues, stop and explicitly ask the user whether to keep iterating on the same change or finalize and archive it
|
|
275
|
-
8. **Finalization / Archive**: Before archiving, verify the delta specs reflect the final implementation — if iteration changed behavior or scope, re-delegate to **`@spec-writer`** to update the delta specs first, so the merge to source-of-truth is accurate. Check the seed freshness before closing, following the canonical rule in the `LOCKED: seed-freshness` block of `AGENTS.md`: delegate the read-only state check to **`@technical-discovery`** and regenerate only with explicit user approval, never automatically. This closure check protects future sessions from a silently aging seed. Then
|
|
276
|
-
9. **
|
|
277
|
-
10. **Report**: Return final status or blocking findings to the user after automatic storage, an explicit decline, or User-Approved Record completes.
|
|
275
|
+
8. **Finalization / Archive ∥ Memory Record (in parallel)**: Before archiving, verify the delta specs reflect the final implementation — if iteration changed behavior or scope, re-delegate to **`@spec-writer`** to update the delta specs first, so the merge to source-of-truth is accurate. Check the seed freshness before closing, following the canonical rule in the `LOCKED: seed-freshness` block of `AGENTS.md`: delegate the read-only state check to **`@technical-discovery`** and regenerate only with explicit user approval, never automatically. This closure check protects future sessions from a silently aging seed. Then, after explicit user approval to archive, start **`@documenter`** and — only when a concrete, plausible memory candidate exists — **`@memory-keeper`** (Automatic Record) **in parallel**: they write disjoint surfaces (`aspec/` for `@documenter`, `.ancleto/memory.db` for `@memory-keeper`) and neither needs the other's output — the change name the record needs is known before the archive — so do not wait for one to finish before starting the other. With no real candidate, delegate only `@documenter`: do not delegate `@memory-keeper`, do not invoke Optional Draft, and do not ask the user about memory (one memory call maximum). If the record would need the archive result, do not parallelize and report the blocker. Collect and report the state of both stages.
|
|
276
|
+
9. **Report**: Return final status or blocking findings to the user once both finalization stages settle, or after the archive alone when there was no memory candidate.
|
|
278
277
|
|
|
279
278
|
### Path B: Direct-Implementation Changes
|
|
280
279
|
|
|
@@ -282,17 +281,17 @@ Before any write-capable stage, apply the **Runtime Safety** rule declared once
|
|
|
282
281
|
2. **WAIT**: Do not delegate to `@coder` until the user responds explicitly
|
|
283
282
|
3. **NO IMPLIED CONFIRMATION**: Do not assume approval from silence, delay, or lack of objection
|
|
284
283
|
4. **Implementation**: Delegate to **`@coder`** only after explicit user confirmation of the direct path. Include the complete Resolved Context Envelope.
|
|
285
|
-
5. **Validation**: Delegate to **`@tester`** with the complete Resolved Context Envelope, the coder's task-owned files, and its reported risks. Require a non-writing format check and one lint pass after all edits. Preserve the tester's final task-owned file union and Validation Ledger. A format failure in a task-owned file is failed verification and must not be fixed silently by `@tester`. Delegate to **`@reviewer`** when the completed direct change modifies user-visible behavior or user-facing content that may already be documented in a source-of-truth spec. For internal changes without observable behavior impact, independent review remains optional. Include the complete envelope,
|
|
284
|
+
5. **Validation**: Delegate to **`@tester`** with the complete Resolved Context Envelope, the coder's task-owned files, and its reported risks. Require a non-writing format check and one lint pass after all edits. Preserve the tester's final task-owned file union, its deterministic `affected-set` (dependents included), and its closed Validation Ledger. A format failure in a task-owned file is failed verification and must not be fixed silently by `@tester`. Delegate to **`@reviewer`** when the completed direct change modifies user-visible behavior or user-facing content that may already be documented in a source-of-truth spec. For internal changes without observable behavior impact, independent review remains optional. Include the complete envelope, the `affected-set` as the review scope, and the closed ledger. `@reviewer` remains the only agent responsible for checking whether an existing source-of-truth spec requires an update.
|
|
286
285
|
6. **🛑 SPEC DOCUMENTATION CHECKPOINT**: If `@reviewer` raised a `SPEC UPDATE RECOMMENDED` flag, STOP and ask the user whether to update the affected source-of-truth spec. Do not assume approval from silence, delay, or lack of objection
|
|
287
286
|
7. **Spec Documentation**: Only after explicit user approval, delegate to **`@documenter`** in `Standalone Source-of-Truth Update` mode to update `aspec/specs/{capability}/spec.md`
|
|
288
|
-
8. **Memory Record**: Delegate to **`@memory-keeper`** in Automatic Record mode when a concrete, plausible candidate exists, with the factual completed-work summary and validation evidence supporting it.
|
|
289
|
-
9. **Report**: Return final status or blocking findings to the user after automatic storage
|
|
287
|
+
8. **Memory Record (one step, only with a candidate)**: Make at most **one** memory call. Delegate to **`@memory-keeper`** in Automatic Record mode only when a concrete, plausible candidate exists, with the factual completed-work summary and validation evidence supporting it. With no real candidate, do not delegate `@memory-keeper`, do not invoke Optional Draft, do not ask the user about memory, and finish without storing. Reserve User-Approved Record for an explicit user request to store a specific text.
|
|
288
|
+
9. **Report**: Return final status or blocking findings to the user after automatic storage or after finishing without storing. When a source-of-truth spec was updated, explicitly highlight in the summary that documentation was left for this direct change, naming the updated spec file
|
|
290
289
|
|
|
291
290
|
### Path C: Direct-Test-Only Changes
|
|
292
291
|
|
|
293
|
-
1. **Testing Work**: Delegate to **`@tester`** only, with the complete Resolved Context Envelope. Preserve its final task-owned file union and Validation Ledger.
|
|
292
|
+
1. **Testing Work**: Delegate to **`@tester`** only, with the complete Resolved Context Envelope. Preserve its final task-owned file union, its `affected-set`, and its Validation Ledger.
|
|
294
293
|
2. **Escalation Check**: If `@tester` discovers that the request actually requires product-code changes or broader scope, stop and reclassify through the orchestrator
|
|
295
|
-
3. **Review**: Delegate to **`@reviewer`** only when an independent review is useful, with the complete envelope,
|
|
294
|
+
3. **Review**: Delegate to **`@reviewer`** only when an independent review is useful, with the complete envelope, the `affected-set` as the review scope, and the Validation Ledger
|
|
296
295
|
4. **Memory Record**: Only when a concrete, plausible candidate lesson exists, delegate to **`@memory-keeper`** in Automatic Record mode with that candidate, its factual completed-work summary, validation evidence, and available card context. If it returns `no-entry-warranted`, report that result and do not offer the optional fallback.
|
|
297
296
|
5. **Report**: Return final status or blocking findings to the user
|
|
298
297
|
|
|
@@ -300,9 +299,9 @@ Before any write-capable stage, apply the **Runtime Safety** rule declared once
|
|
|
300
299
|
|
|
301
300
|
Before advancing to the next stage, verify that each subagent returned a usable result.
|
|
302
301
|
|
|
303
|
-
`@tester` returns the final `task-owned files` union
|
|
302
|
+
`@tester` returns the final `task-owned files` union (the coder's files plus any tests it modified) and the deterministic `affected-set`: the closure of dependents on the import graph, seeded by those files, plus the files themselves. The `affected-set` is the scope of review — it includes the dependents, not only the changed files. Do not treat pre-existing changes outside that set as task findings.
|
|
304
303
|
|
|
305
|
-
For each validation command in the tester result, including format check and lint, preserve a Validation Ledger entry with: command, outcome (`passed`, `task-regression`, `pre-existing-unrelated`, or `inconclusive`), and related files when it failed.
|
|
304
|
+
For each validation command in the tester result, including format check and lint, preserve a Validation Ledger entry with: command, outcome (`passed`, `task-regression`, `pre-existing-unrelated`, `not-configured`, or `inconclusive`), and related files when it failed. A `not-configured` entry (the project declares no formatter, or no such command) is a terminal non-failure that closes the ledger for that command: it is not a missing entry and does not block the review. Verify every scope command has a terminal entry before delegating the review, then pass the closed ledger unchanged to `@reviewer`. A confirmed `pre-existing-unrelated` result is reported once in the final status as a non-blocking residual limitation; do not stop, re-run it, or request a correction unless a later relevant change invalidates that classification.
|
|
306
305
|
|
|
307
306
|
### Handling subagent returns
|
|
308
307
|
|
package/agents/reviewer.md
CHANGED
|
@@ -95,16 +95,16 @@ If the received context is not enough to resolve the write surface or the read-m
|
|
|
95
95
|
|
|
96
96
|
1. Identify whether the task is `aspec Change`, `direct-implementation`, or `direct-test-only`
|
|
97
97
|
2. Read only the minimum relevant approved inputs and changed implementation files. When the change signals a guarantee (`S1 ∧ S2`, `aspec Change` mode only), that minimum additionally includes the source of the operations that can mutate the protected state, even beyond the diff
|
|
98
|
-
3. Review the
|
|
98
|
+
3. Review the `affected-set` and its tests against the prearmed, closed Validation Ledger supplied for the approved scope
|
|
99
99
|
4. Check for correctness issues, missing coverage, convention violations, and scope creep
|
|
100
|
-
5.
|
|
100
|
+
5. Treat the prearmed Validation Ledger as evidence: consume it without reconstructing or re-running anything. Do not request a re-delegation to `@tester` for command evidence that is missing or inconclusive **within the covered scope** — a ledger that does not cover a scope command is an orchestrator precondition failure, not a reviewer request. You still report missing **context** (not command evidence) needed for the guarantee verification. You do not have Bash and must not run commands yourself
|
|
101
101
|
6. Return a short structured review report
|
|
102
102
|
|
|
103
103
|
## Scope Review Rules
|
|
104
104
|
|
|
105
105
|
Review the changed files against the approved request or aspec scope.
|
|
106
106
|
|
|
107
|
-
Review scope is
|
|
107
|
+
Review scope is the `affected-set` supplied by `@orchestrator` — the deterministically propagated closure of dependents, including the `task-owned files` themselves — not just the changed files. Do not report pre-existing worktree changes outside that set as warnings or suggest removing them from a commit.
|
|
108
108
|
|
|
109
109
|
This scope limit does not apply to a `GUARANTEE NOT SUSTAINED` finding: a guarantee can be falsified by an operation outside the `task-owned files` and outside the diff, and the finding is not discarded or downgraded for that reason. See "Guarantee Verification Under the Real Model".
|
|
110
110
|
|
|
@@ -167,7 +167,7 @@ Do not block on minor style nits when the change is otherwise correct.
|
|
|
167
167
|
|
|
168
168
|
## Validation Evidence
|
|
169
169
|
|
|
170
|
-
|
|
170
|
+
The tester's prearmed Validation Ledger is the authoritative evidence for the covered scope: consume it as-is, and do not reconstruct or summarize it. A `pre-existing-unrelated` entry is a non-blocking residual limitation: do not elevate it to a warning unless the review finds evidence that it affects task-owned files. A `not-configured` entry is a registered non-failure (the project declares no such command): it closes the ledger for that command and is not missing evidence. When the ledger covers a scope command as `inconclusive`, treat it as evidence and do not request a re-delegation to `@tester` within the covered scope. Report missing **context** (not command evidence) needed to resolve the guarantee verification. You do not have Bash and must not run commands or external requests.
|
|
171
171
|
|
|
172
172
|
## Memory Candidate Review
|
|
173
173
|
|
|
@@ -194,7 +194,7 @@ Return a short structured report with:
|
|
|
194
194
|
- `SPEC UPDATE RECOMMENDED` flag when a `direct-implementation` change modified documented behavior
|
|
195
195
|
- `GUARANTEE NOT SUSTAINED` flag when the proposal's guarantee does not hold under the real model (`aspec Change` mode only), naming the guarantee, the falsifying operation, and the protected state
|
|
196
196
|
- `MEMORY CANDIDATE`: one lesson worth keeping in the team memory, or "none" with the reason automatic storage is not recommended
|
|
197
|
-
- missing
|
|
197
|
+
- missing **context** for the guarantee verification (not command evidence; the prearmed ledger covers the review scope), or "none"
|
|
198
198
|
- overall review verdict
|
|
199
199
|
|
|
200
200
|
**Output cap**: at most 5 findings per severity (`CRITICAL`, `WARNING`, `SUGGESTION`); if more exist, keep the 5 highest-impact and state the count of the rest, offering detail on request. `Summary` max 5 lines. Cite `file:line`; never paste diffs or code.
|
package/agents/tester.md
CHANGED
|
@@ -67,7 +67,8 @@ When reporting results:
|
|
|
67
67
|
- state whether they passed or failed
|
|
68
68
|
- summarize any failures or gaps that remain
|
|
69
69
|
- clearly separate verified behavior from unverified assumptions
|
|
70
|
-
- classify every command in the Validation Ledger as `passed`, `task-regression`, `pre-existing-unrelated`, or `inconclusive`
|
|
70
|
+
- classify every command in the Validation Ledger as `passed`, `task-regression`, `pre-existing-unrelated`, `not-configured`, or `inconclusive`
|
|
71
|
+
- return a **complete and closed** Validation Ledger for the review scope: every command relevant to it —the change's tests, the tests of the affected set, and the declared format check and lint— has an entry with a terminal classification. A `not-configured` entry (the project declares no formatter, or no such command) is a registered non-failure that closes that command, consistent with the `LOCKED: validation-commands` policy; it is not a missing entry and does not block the review
|
|
71
72
|
|
|
72
73
|
## Input Modes
|
|
73
74
|
|
|
@@ -130,6 +131,8 @@ Prefer the smallest command that proves the change AND catches regressions in th
|
|
|
130
131
|
|
|
131
132
|
Being economical means running the affected set, not the whole repo — it does NOT mean narrowing to only the changed files. Collateral breakage in dependent code is a real risk and must be caught, not skipped as "out of scope".
|
|
132
133
|
|
|
134
|
+
The affected set is the deterministic `affected-set`: the transitive closure of **dependents** on the project's import graph, seeded by the task-owned files, plus those files themselves. Compute it with the mechanism the project declares in `PRODUCT.md` → `## Project Commands`; when the project declares none, use the read-only CLI `ancleto affected [--cwd <dir>] [--json] <file...>`. Its import graph covers the static forms `import`, `import … from`, `export … from`, `require(...)` and `import(...)` over `.js/.mjs/.cjs/.ts/.tsx`, respects the project's ignores, and is reproducible: the same repository state plus the same seed yields the same set. Report the computed `affected-set` as your verification target. Non-relative specifiers (npm packages, Node builtins, and path aliases) are **external by construction**: they never generate a dependency edge and are never `inconclusive`; the read-only surface reports a **count** of them as data, not as a warning. The graph scans only the call forms `import(` and `require(` and classifies each **by its argument**, not by its content: when the argument —after spaces and comments— is a **pure string literal followed by `)`**, the call is static (category 1 if the literal is a resolvable relative, category 2 if it is non-relative); **any other argument** —a bare expression, a **literal prefix followed by an expression** (`'./x' + y`), a template with an expression, a parenthesized argument, etc.— is `inconclusive`. The classifier is **conservative by design**: when in doubt it reports `inconclusive`, never silence, and it promises nothing about dynamics it does not scan. An **unresolved relative** specifier is also `inconclusive`; do not assume any `inconclusive` case covered. To cover path aliases, the project must declare its own affected mechanism in `PRODUCT.md` → `## Project Commands`.
|
|
135
|
+
|
|
133
136
|
Run each command at most once after the last relevant edit. Do not rerun a passed command or a confirmed pre-existing failure unless the files relevant to it changed. Run the project's declared format check (if any) without `--write`, then the declared lint once against the combined task-owned file set; if the project declares no formatter, the format check is `not-configured`. Do not format production files silently, and do not check paths excluded by the repository configuration.
|
|
134
137
|
|
|
135
138
|
## Test Structure Rules
|
|
@@ -161,7 +164,8 @@ After validation, return a short structured summary including:
|
|
|
161
164
|
- tests created or updated
|
|
162
165
|
- `task-owned test files`: the exact tests modified during this delegation
|
|
163
166
|
- `final task-owned files`: the coder's task-owned files plus tests modified during this delegation
|
|
164
|
-
- `
|
|
167
|
+
- `affected-set`: the deterministic closure of dependents (plus the files themselves) used as the verification target, together with the count of non-relative (external) specifiers reported as data and the inconclusive cases (`import(...)` / `require(...)` calls whose argument is not a pure string literal, and unresolved relatives) reported as `unresolved`
|
|
168
|
+
- `Validation Ledger`: complete and closed for the review scope — each command, outcome classification (including `not-configured`), and related files when it failed
|
|
165
169
|
- verification result
|
|
166
170
|
- whether reclassification was required
|
|
167
171
|
- failed verification items, if any
|
package/package.json
CHANGED
package/src/cli/index.js
CHANGED
|
@@ -14,6 +14,7 @@ import { writeWorkingContext, writeWorkingContextBlock, renderWorkingContextBloc
|
|
|
14
14
|
import { memoryDoctor } from '../core/memory/doctor.js'
|
|
15
15
|
import { serveMemoryMcp } from '../core/memory/mcp-server.js'
|
|
16
16
|
import { writeDiscoveryMap } from '../core/discovery.js'
|
|
17
|
+
import { computeAffectedSet } from '../core/affected-set.js'
|
|
17
18
|
import { readProjectTier, buildRepomixArgs, tierTokenBudget } from '../core/repomix-tier.js'
|
|
18
19
|
import { showBanner, selectOption, selectMultiple, stopBanner } from './ui.js'
|
|
19
20
|
import { tierModels, gratisModel, envGratisModel, isKnownGratisModel, MUSE_SPARK_MODEL, GRATIS_FALLBACK_MODEL } from '../core/tier-models.js'
|
|
@@ -59,6 +60,10 @@ Uso:
|
|
|
59
60
|
ancleto specs check [--change <name>] [--json]
|
|
60
61
|
Valida keywords canonicos (### Requirement:, WHEN, THEN)
|
|
61
62
|
en aspec/specs y, con --change, en los deltas del change
|
|
63
|
+
ancleto affected [--cwd <dir>] [--json] <archivo...>
|
|
64
|
+
Computa el affected-set (clausura de dependientes por el grafo
|
|
65
|
+
de imports) sembrado por los archivos dados (read-only; reporta
|
|
66
|
+
el conteo de specifiers externos no relativos como dato)
|
|
62
67
|
ancleto stats [--all] [--limit N] [--since YYYY-MM-DD] [--session <id>] [--json]
|
|
63
68
|
Tokens por sesion de opencode (entrada/salida/razonamiento/cache),
|
|
64
69
|
con rollup de subagentes y desglose por agente con --session
|
|
@@ -2598,6 +2603,50 @@ async function specsCheckCommand(args) {
|
|
|
2598
2603
|
process.exit(ok ? 0 : 1)
|
|
2599
2604
|
}
|
|
2600
2605
|
|
|
2606
|
+
// Read-only: computa el affected-set (dependientes incluidos) sin escribir nada.
|
|
2607
|
+
async function affectedCommand(args) {
|
|
2608
|
+
const asJson = args.includes('--json')
|
|
2609
|
+
const ci = args.indexOf('--cwd')
|
|
2610
|
+
if (ci >= 0 && ci + 1 >= args.length) {
|
|
2611
|
+
console.error('ancleto: uso: ancleto affected [--cwd <dir>] [--json] <archivo...>')
|
|
2612
|
+
process.exit(1)
|
|
2613
|
+
}
|
|
2614
|
+
const cwd = ci >= 0 ? resolve(args[ci + 1]) : process.cwd()
|
|
2615
|
+
const seeds = []
|
|
2616
|
+
for (let i = 0; i < args.length; i++) {
|
|
2617
|
+
const a = args[i]
|
|
2618
|
+
if (a === '--json') continue
|
|
2619
|
+
if (a === '--cwd') {
|
|
2620
|
+
i += 1
|
|
2621
|
+
continue
|
|
2622
|
+
}
|
|
2623
|
+
seeds.push(a)
|
|
2624
|
+
}
|
|
2625
|
+
if (seeds.length === 0) {
|
|
2626
|
+
console.error('ancleto: uso: ancleto affected [--cwd <dir>] [--json] <archivo...>')
|
|
2627
|
+
process.exit(1)
|
|
2628
|
+
}
|
|
2629
|
+
const { seeds: normalizedSeeds, affected, unresolved, externalCount } = computeAffectedSet({
|
|
2630
|
+
rootDir: cwd,
|
|
2631
|
+
seeds
|
|
2632
|
+
})
|
|
2633
|
+
if (asJson) {
|
|
2634
|
+
console.log(JSON.stringify({ seeds: normalizedSeeds, affected, unresolved, externalCount }, null, 2))
|
|
2635
|
+
return
|
|
2636
|
+
}
|
|
2637
|
+
for (const f of affected) console.log(f)
|
|
2638
|
+
// Dato informativo (no warning ni error): cuantos specifiers no relativos se
|
|
2639
|
+
// trataron como externos. Va a stderr para no ensuciar la lista de stdout y
|
|
2640
|
+
// se omite cuando el conteo es 0 (el campo `externalCount` sigue en --json).
|
|
2641
|
+
if (externalCount > 0) {
|
|
2642
|
+
console.error(`ancleto: affected: ${externalCount} specifier(s) externos (no relativos; dato informativo)`)
|
|
2643
|
+
}
|
|
2644
|
+
if (unresolved.length > 0) {
|
|
2645
|
+
console.error(`ancleto: affected: ${unresolved.length} import(s) no resueltos (inconclusos)`)
|
|
2646
|
+
for (const u of unresolved) console.error(` ? ${u.file}: ${u.specifier}`)
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
|
|
2601
2650
|
function opencodeDbPath() {
|
|
2602
2651
|
return process.env.ANCLETO_OPENCODE_DB || join(homedir(), '.local', 'share', 'opencode', 'opencode.db')
|
|
2603
2652
|
}
|
|
@@ -3101,6 +3150,9 @@ switch (cmd) {
|
|
|
3101
3150
|
case 'specs':
|
|
3102
3151
|
await specsCheckCommand(rest)
|
|
3103
3152
|
break
|
|
3153
|
+
case 'affected':
|
|
3154
|
+
await affectedCommand(rest)
|
|
3155
|
+
break
|
|
3104
3156
|
case 'stats':
|
|
3105
3157
|
await statsCommand(rest)
|
|
3106
3158
|
break
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { readFileSync, readdirSync } from 'node:fs'
|
|
2
|
+
import { dirname, join, relative, resolve } from 'node:path'
|
|
3
|
+
import { TOPOLOGY_IGNORED_DIRS } from './discovery.js'
|
|
4
|
+
|
|
5
|
+
// Mecanismo del affected-set: clausura transitiva de DEPENDIENTES sobre el grafo
|
|
6
|
+
// de imports del proyecto, sembrada por los archivos tocados. Zero-dep y
|
|
7
|
+
// determinista (mismo estado + misma semilla => mismo conjunto).
|
|
8
|
+
//
|
|
9
|
+
// Cobertura: formas estáticas de `import`, `import ... from`, `export ... from`
|
|
10
|
+
// y las llamadas `import(...)` / `require(...)` cuyo argumento es un literal
|
|
11
|
+
// puro (string o template sin expresiones), sobre .js/.mjs/.cjs/.ts/.tsx.
|
|
12
|
+
// Alcance escaneado: sólo las formas de llamada `import(` / `require(`. Otras
|
|
13
|
+
// formas de invocar un import/require dinámico (`require?.(…)`, `(require)(…)`,
|
|
14
|
+
// el acceso computed `obj['require'](…)`) NO se escanean ni se reportan: se
|
|
15
|
+
// delegan al mecanismo declarado por el proyecto. El acceso por miembro
|
|
16
|
+
// `obj.require(…)` sí se matchea léxicamente (`\brequire\b` sin lookbehind):
|
|
17
|
+
// sobre-inclusión conservadora, en la dirección segura, nunca un hueco.
|
|
18
|
+
// Acotación por diseño, no un hueco.
|
|
19
|
+
// Clasificación conservadora por ARGUMENTO, no por contenido de la llamada:
|
|
20
|
+
// sólo una llamada con literal puro —tras espacios y comentarios— seguido de
|
|
21
|
+
// `)` se trata como estática; cualquier otro argumento (expresión pelada,
|
|
22
|
+
// `'./x' + y`, template con `${}`, paréntesis, etc.) se reporta como inconcluso
|
|
23
|
+
// (`unresolved`), nunca en silencio. Los especificadores NO relativos (paquetes,
|
|
24
|
+
// builtins, alias de paths) son externos al grafo y no se resuelven; se cuentan
|
|
25
|
+
// como dato (`externalCount`) para hacer visible la frontera, sin tratarlos como
|
|
26
|
+
// warning ni error. Los relativos que no resuelven también se reportan como
|
|
27
|
+
// inconclusos.
|
|
28
|
+
|
|
29
|
+
export const SOURCE_EXTENSIONS = ['.js', '.mjs', '.cjs', '.ts', '.tsx']
|
|
30
|
+
|
|
31
|
+
// Formas estáticas con specifier literal inline (no son llamadas): el argumento
|
|
32
|
+
// de las llamadas `import(...)` / `require(...)` se clasifica aparte.
|
|
33
|
+
const SPECIFIER_PATTERNS = [
|
|
34
|
+
/\bimport\s+[^'"]*?\bfrom\s*['"]([^'"]+)['"]/g,
|
|
35
|
+
/\bimport\s*['"]([^'"]+)['"]/g,
|
|
36
|
+
/\bexport\s+[^'"]*?\bfrom\s*['"]([^'"]+)['"]/g
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
// Únicas palabras de llamada escaneadas: `import` y `require`. El hueco entre la
|
|
40
|
+
// palabra y el `(` se recorre con skipTrivia (espacios + comentarios), de modo que
|
|
41
|
+
// `require/*c*/('x')` e `import/*c*/('x')` también entran al clasificador. No son
|
|
42
|
+
// llamadas —y se ignoran— `import.meta`, el `import` estático y `require.resolve(`,
|
|
43
|
+
// porque tras la palabra (saltando trivia) el próximo carácter significativo no es `(`.
|
|
44
|
+
const CALL_FORMS = [
|
|
45
|
+
{ re: /\bimport\b/g, kind: 'import' },
|
|
46
|
+
{ re: /\brequire\b/g, kind: 'require' }
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
const toPosix = (p) => String(p).replace(/\\/g, '/')
|
|
50
|
+
|
|
51
|
+
export function isSourceFile(name) {
|
|
52
|
+
return SOURCE_EXTENSIONS.some((ext) => name.endsWith(ext))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Recorre rootDir juntando archivos fuente, respetando los ignores del proyecto
|
|
56
|
+
// (los mismos que usa el discovery map).
|
|
57
|
+
export function collectSourceFiles(rootDir, ignored = TOPOLOGY_IGNORED_DIRS) {
|
|
58
|
+
const ignoredSet = new Set(ignored)
|
|
59
|
+
const out = []
|
|
60
|
+
const walk = (dir) => {
|
|
61
|
+
let entries
|
|
62
|
+
try {
|
|
63
|
+
entries = readdirSync(dir, { withFileTypes: true })
|
|
64
|
+
} catch {
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
for (const e of entries) {
|
|
68
|
+
if (e.isDirectory()) {
|
|
69
|
+
if (ignoredSet.has(e.name)) continue
|
|
70
|
+
walk(join(dir, e.name))
|
|
71
|
+
} else if (e.isFile() && isSourceFile(e.name)) {
|
|
72
|
+
out.push(join(dir, e.name))
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
walk(rootDir)
|
|
77
|
+
out.sort()
|
|
78
|
+
return out
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const isSpace = (c) => c === ' ' || c === '\t' || c === '\n' || c === '\r' || c === '\f' || c === '\v'
|
|
82
|
+
|
|
83
|
+
// Avanza sobre espacios y comentarios (`/* */` y `//`). Devuelve el índice del
|
|
84
|
+
// próximo carácter significativo, o null si un comentario de bloque no cierra.
|
|
85
|
+
function skipTrivia(content, i) {
|
|
86
|
+
let j = i
|
|
87
|
+
while (j < content.length) {
|
|
88
|
+
const c = content[j]
|
|
89
|
+
if (isSpace(c)) {
|
|
90
|
+
j += 1
|
|
91
|
+
continue
|
|
92
|
+
}
|
|
93
|
+
if (c === '/' && content[j + 1] === '*') {
|
|
94
|
+
const end = content.indexOf('*/', j + 2)
|
|
95
|
+
if (end === -1) return null
|
|
96
|
+
j = end + 2
|
|
97
|
+
continue
|
|
98
|
+
}
|
|
99
|
+
if (c === '/' && content[j + 1] === '/') {
|
|
100
|
+
const end = content.indexOf('\n', j + 2)
|
|
101
|
+
j = end === -1 ? content.length : end + 1
|
|
102
|
+
continue
|
|
103
|
+
}
|
|
104
|
+
break
|
|
105
|
+
}
|
|
106
|
+
return j
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Lee un string literal de comilla simple o doble desde su apertura; null si no
|
|
110
|
+
// cierra.
|
|
111
|
+
function readQuoted(content, i, quote) {
|
|
112
|
+
let value = ''
|
|
113
|
+
let j = i + 1
|
|
114
|
+
while (j < content.length) {
|
|
115
|
+
const c = content[j]
|
|
116
|
+
if (c === '\\') {
|
|
117
|
+
value += content[j + 1] ?? ''
|
|
118
|
+
j += 2
|
|
119
|
+
continue
|
|
120
|
+
}
|
|
121
|
+
if (c === quote) return { value, next: j + 1 }
|
|
122
|
+
value += c
|
|
123
|
+
j += 1
|
|
124
|
+
}
|
|
125
|
+
return null
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Lee un template literal; null si no cierra o si contiene una expresión `${}`.
|
|
129
|
+
function readTemplate(content, i) {
|
|
130
|
+
let value = ''
|
|
131
|
+
let j = i + 1
|
|
132
|
+
while (j < content.length) {
|
|
133
|
+
const c = content[j]
|
|
134
|
+
if (c === '\\') {
|
|
135
|
+
value += content[j + 1] ?? ''
|
|
136
|
+
j += 2
|
|
137
|
+
continue
|
|
138
|
+
}
|
|
139
|
+
if (c === '`') return { value, next: j + 1 }
|
|
140
|
+
if (c === '$' && content[j + 1] === '{') return null
|
|
141
|
+
value += c
|
|
142
|
+
j += 1
|
|
143
|
+
}
|
|
144
|
+
return null
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Clasifica el argumento de una llamada cuyo `(` está en openParen. Devuelve el
|
|
148
|
+
// specifier si —tras espacios y comentarios— el argumento es un string o
|
|
149
|
+
// template sin expresión seguido de `)`; null en cualquier otro caso (argumento
|
|
150
|
+
// no literal), de modo que el caso se reporte como inconcluso.
|
|
151
|
+
function staticCallSpecifier(content, openParen) {
|
|
152
|
+
const start = skipTrivia(content, openParen + 1)
|
|
153
|
+
if (start === null || start >= content.length) return null
|
|
154
|
+
const quote = content[start]
|
|
155
|
+
let literal = null
|
|
156
|
+
if (quote === "'" || quote === '"') literal = readQuoted(content, start, quote)
|
|
157
|
+
else if (quote === '`') literal = readTemplate(content, start)
|
|
158
|
+
if (literal === null) return null
|
|
159
|
+
const closing = skipTrivia(content, literal.next)
|
|
160
|
+
if (closing === null || content[closing] !== ')') return null
|
|
161
|
+
return literal.value
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Devuelve los especificadores estáticos hallados (import/export … from,
|
|
165
|
+
// `import '…'` y las llamadas `import(...)` / `require(...)` con literal puro) y
|
|
166
|
+
// el conteo de llamadas con argumento no literal, por forma.
|
|
167
|
+
export function extractSpecifiers(content) {
|
|
168
|
+
const specifiers = []
|
|
169
|
+
for (const pattern of SPECIFIER_PATTERNS) {
|
|
170
|
+
pattern.lastIndex = 0
|
|
171
|
+
let m
|
|
172
|
+
while ((m = pattern.exec(content)) !== null) {
|
|
173
|
+
specifiers.push(m[1])
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
let dynamicUnresolved = 0
|
|
177
|
+
let dynamicRequireUnresolved = 0
|
|
178
|
+
for (const { re, kind } of CALL_FORMS) {
|
|
179
|
+
re.lastIndex = 0
|
|
180
|
+
let m
|
|
181
|
+
while ((m = re.exec(content)) !== null) {
|
|
182
|
+
// Cierra el hueco palabra→`(` saltando espacios y comentarios: una palabra
|
|
183
|
+
// `import`/`require` sólo es una llamada si el próximo carácter significativo
|
|
184
|
+
// es `(`; si no (`.`, `{`, `'`, EOF…), no es una llamada y se ignora.
|
|
185
|
+
const openParen = skipTrivia(content, m.index + m[0].length)
|
|
186
|
+
if (openParen === null || content[openParen] !== '(') continue
|
|
187
|
+
const specifier = staticCallSpecifier(content, openParen)
|
|
188
|
+
if (specifier !== null) {
|
|
189
|
+
specifiers.push(specifier)
|
|
190
|
+
} else if (kind === 'import') {
|
|
191
|
+
dynamicUnresolved += 1
|
|
192
|
+
} else {
|
|
193
|
+
dynamicRequireUnresolved += 1
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return { specifiers, dynamicUnresolved, dynamicRequireUnresolved }
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function resolveRelative(fromFile, spec, fileSet) {
|
|
201
|
+
if (!spec.startsWith('./') && !spec.startsWith('../')) return null
|
|
202
|
+
const base = join(dirname(fromFile), spec)
|
|
203
|
+
const candidates = [base]
|
|
204
|
+
for (const ext of SOURCE_EXTENSIONS) candidates.push(base + ext)
|
|
205
|
+
for (const ext of SOURCE_EXTENSIONS) candidates.push(join(base, 'index' + ext))
|
|
206
|
+
for (const c of candidates) {
|
|
207
|
+
if (fileSet.has(c)) return c
|
|
208
|
+
}
|
|
209
|
+
return null
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Grafo de imports: aristas src -> deps (relativas, posix) y lista de casos
|
|
213
|
+
// inconclusos. `sources` son los nodos del grafo.
|
|
214
|
+
export function buildImportGraph(rootDir, ignored = TOPOLOGY_IGNORED_DIRS) {
|
|
215
|
+
const files = collectSourceFiles(rootDir, ignored)
|
|
216
|
+
const fileSet = new Set(files)
|
|
217
|
+
const edges = new Map()
|
|
218
|
+
const unresolved = []
|
|
219
|
+
let externalCount = 0
|
|
220
|
+
for (const abs of files) {
|
|
221
|
+
const rel = toPosix(relative(rootDir, abs))
|
|
222
|
+
const deps = new Set()
|
|
223
|
+
let content
|
|
224
|
+
try {
|
|
225
|
+
content = readFileSync(abs, 'utf8')
|
|
226
|
+
} catch {
|
|
227
|
+
edges.set(rel, deps)
|
|
228
|
+
continue
|
|
229
|
+
}
|
|
230
|
+
const { specifiers, dynamicUnresolved, dynamicRequireUnresolved } = extractSpecifiers(content)
|
|
231
|
+
for (const spec of specifiers) {
|
|
232
|
+
const resolved = resolveRelative(abs, spec, fileSet)
|
|
233
|
+
if (resolved) deps.add(toPosix(relative(rootDir, resolved)))
|
|
234
|
+
else if (spec.startsWith('./') || spec.startsWith('../')) {
|
|
235
|
+
unresolved.push({ file: rel, specifier: spec })
|
|
236
|
+
} else {
|
|
237
|
+
// No relativo no resuelto => externo (paquete/builtin/alias): fuera del
|
|
238
|
+
// grafo. Se cuenta como dato, no se resuelve ni se reporta inconcluso.
|
|
239
|
+
externalCount += 1
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (dynamicUnresolved > 0) {
|
|
243
|
+
unresolved.push({ file: rel, specifier: 'import(<dynamic>)' })
|
|
244
|
+
}
|
|
245
|
+
if (dynamicRequireUnresolved > 0) {
|
|
246
|
+
unresolved.push({ file: rel, specifier: 'require(<dynamic>)' })
|
|
247
|
+
}
|
|
248
|
+
edges.set(rel, deps)
|
|
249
|
+
}
|
|
250
|
+
return { edges, sources: new Set(edges.keys()), unresolved, externalCount }
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function normalizeSeed(rootDir, seed) {
|
|
254
|
+
return toPosix(relative(rootDir, resolve(rootDir, seed)))
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// affected-set = semilla + clausura transitiva de dependientes. Incluye siempre
|
|
258
|
+
// los archivos sembrados, aunque no sean nodos del grafo (p. ej. un cambio que
|
|
259
|
+
// sólo toca documentación).
|
|
260
|
+
export function computeAffectedSet({ rootDir, seeds, ignored = TOPOLOGY_IGNORED_DIRS }) {
|
|
261
|
+
const { edges, unresolved, externalCount } = buildImportGraph(rootDir, ignored)
|
|
262
|
+
const dependents = new Map()
|
|
263
|
+
for (const [file, deps] of edges) {
|
|
264
|
+
for (const dep of deps) {
|
|
265
|
+
if (!dependents.has(dep)) dependents.set(dep, new Set())
|
|
266
|
+
dependents.get(dep).add(file)
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
const seedSet = new Set(seeds.map((s) => normalizeSeed(rootDir, s)))
|
|
270
|
+
const affected = new Set(seedSet)
|
|
271
|
+
const queue = [...seedSet]
|
|
272
|
+
while (queue.length > 0) {
|
|
273
|
+
const cur = queue.shift()
|
|
274
|
+
for (const dep of dependents.get(cur) || []) {
|
|
275
|
+
if (!affected.has(dep)) {
|
|
276
|
+
affected.add(dep)
|
|
277
|
+
queue.push(dep)
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
unresolved.sort((a, b) => a.file.localeCompare(b.file) || a.specifier.localeCompare(b.specifier))
|
|
282
|
+
return {
|
|
283
|
+
seeds: [...seedSet].sort(),
|
|
284
|
+
affected: [...affected].sort(),
|
|
285
|
+
unresolved,
|
|
286
|
+
externalCount
|
|
287
|
+
}
|
|
288
|
+
}
|