mandrel 1.93.0 → 1.94.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.
- package/.agents/agents/acceptance-critic.md +129 -0
- package/.agents/agents/retro.md +42 -0
- package/.agents/agents/story-worker.md +162 -0
- package/.agents/docs/configuration.md +7 -1
- package/.agents/docs/execution-reference.md +27 -2
- package/.agents/instructions.md +43 -33
- package/.agents/personas/engineer.md +26 -112
- package/.agents/personas/security-engineer.md +1 -2
- package/.agents/rules/git-conventions-reference.md +225 -0
- package/.agents/rules/git-conventions.md +25 -200
- package/.agents/rules/security-baseline.md +5 -0
- package/.agents/rules/testing-standards.md +106 -13
- package/.agents/schemas/agentrc.schema.json +31 -1
- package/.agents/schemas/lifecycle/slice.end.schema.json +21 -0
- package/.agents/schemas/lifecycle/slice.heartbeat.schema.json +20 -0
- package/.agents/schemas/lifecycle/slice.start.schema.json +17 -0
- package/.agents/scripts/acceptance-eval.js +62 -18
- package/.agents/scripts/agents-bootstrap-github.js +1 -1
- package/.agents/scripts/bookkeeping-reconcile.js +117 -0
- package/.agents/scripts/check-context-budget.js +62 -5
- package/.agents/scripts/diagnose-friction.js +0 -6
- package/.agents/scripts/epic-deliver-prepare.js +272 -10
- package/.agents/scripts/lib/bootstrap/project-bootstrap.js +56 -18
- package/.agents/scripts/lib/close-validation/gates.js +159 -21
- package/.agents/scripts/lib/config/acceptance-eval.js +52 -5
- package/.agents/scripts/lib/config/delivery-routing.js +87 -0
- package/.agents/scripts/lib/config/explain.js +2 -0
- package/.agents/scripts/lib/config-resolver.js +1 -1
- package/.agents/scripts/lib/config-settings-schema-delivery.js +37 -3
- package/.agents/scripts/lib/config-settings-schema-quality.js +9 -0
- package/.agents/scripts/lib/doc-tiers.js +37 -2
- package/.agents/scripts/lib/observability/active-story-env.js +111 -2
- package/.agents/scripts/lib/observability/hook-heartbeat.js +219 -0
- package/.agents/scripts/lib/observability/tool-trace-hook.js +15 -4
- package/.agents/scripts/lib/orchestration/acceptance-clusters.js +111 -0
- package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +32 -4
- package/.agents/scripts/lib/orchestration/bookkeeping-outbox.js +270 -0
- package/.agents/scripts/lib/orchestration/ceremony-routing.js +141 -0
- package/.agents/scripts/lib/orchestration/context-hydration-engine.js +3 -124
- package/.agents/scripts/lib/orchestration/deliver-route.js +173 -0
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/authoring-context.js +1 -1
- package/.agents/scripts/lib/orchestration/epic-run-state-store.js +233 -0
- package/.agents/scripts/lib/orchestration/lifecycle/emit-slice-lifecycle.js +270 -0
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/acceptance-reconciler.js +83 -2
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/checkpoint-pointer-writer.js +6 -0
- package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +3 -2
- package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +1 -0
- package/.agents/scripts/lib/orchestration/story-close/pre-merge-validation.js +1 -0
- package/.agents/scripts/lib/orchestration/ticket-validator.js +1 -1
- package/.agents/scripts/lib/provider-factory.js +1 -1
- package/.agents/scripts/lib/templates/decomposer-prompts.js +1 -1
- package/.agents/scripts/post-structured-comment.js +38 -0
- package/.agents/scripts/slice-phase.js +361 -0
- package/.agents/scripts/sync-claude-agents.js +165 -0
- package/.agents/scripts/update-ticket-state.js +31 -0
- package/.agents/scripts/wave-tick.js +138 -9
- package/.agents/skills/core/api-and-interface-design/SKILL.md +5 -3
- package/.agents/skills/core/code-review-and-quality/SKILL.md +63 -7
- package/.agents/skills/core/debugging-and-error-recovery/SKILL.md +1 -1
- package/.agents/skills/core/epic-plan-consolidate/SKILL.md +5 -5
- package/.agents/skills/core/epic-plan-decompose-author/SKILL.md +8 -8
- package/.agents/skills/core/epic-plan-premortem/SKILL.md +4 -4
- package/.agents/skills/core/epic-plan-spec-author/SKILL.md +26 -56
- package/.agents/skills/core/gates-and-baselines/SKILL.md +149 -0
- package/.agents/skills/core/idea-refinement/SKILL.md +2 -8
- package/.agents/skills/core/qa-coverage-mapping/SKILL.md +7 -7
- package/.agents/skills/skills.index.json +11 -381
- package/.agents/workflows/deliver.md +47 -4
- package/.agents/workflows/helpers/acceptance-self-eval.md +38 -13
- package/.agents/workflows/helpers/deliver-epic-reference.md +18 -5
- package/.agents/workflows/helpers/deliver-epic-single.md +331 -0
- package/.agents/workflows/helpers/deliver-epic.md +51 -8
- package/.agents/workflows/helpers/deliver-stories.md +15 -5
- package/.agents/workflows/helpers/epic-deliver-story.md +12 -3
- package/.agents/workflows/helpers/mandrel-sync-config.md +1 -1
- package/.agents/workflows/helpers/plan-epic.md +25 -23
- package/.agents/workflows/mandrel-update.md +1 -1
- package/docs/CHANGELOG.md +16 -0
- package/lib/cli/registry.js +95 -0
- package/package.json +4 -2
- package/.agents/personas/engineer-mobile.md +0 -120
- package/.agents/personas/engineer-web.md +0 -111
- package/.agents/personas/product.md +0 -94
- package/.agents/personas/refactorer.md +0 -113
- package/.agents/personas/sre.md +0 -86
- package/.agents/personas/ux-designer.md +0 -95
- package/.agents/scripts/epic-plan-decompose.js +0 -54
- package/.agents/scripts/epic-plan-spec.js +0 -64
- package/.agents/scripts/lib/orchestration/skill-capsule-loader.js +0 -109
- package/.agents/scripts/plan-critics.js +0 -199
- package/.agents/skills/core/baseline-refresh/SKILL.md +0 -181
- package/.agents/skills/core/ci-cd-and-automation/SKILL.md +0 -274
- package/.agents/skills/core/ci-cd-and-automation/examples.md +0 -211
- package/.agents/skills/core/code-simplification/SKILL.md +0 -389
- package/.agents/skills/core/context-engineering/SKILL.md +0 -309
- package/.agents/skills/core/context-engineering/examples.md +0 -58
- package/.agents/skills/core/deprecation-and-migration/SKILL.md +0 -250
- package/.agents/skills/core/frontend-ui-engineering/SKILL.md +0 -357
- package/.agents/skills/core/hydrate-context/SKILL.md +0 -123
- package/.agents/skills/core/idea-refinement/examples.md +0 -437
- package/.agents/skills/core/idea-refinement/frameworks.md +0 -135
- package/.agents/skills/core/incremental-implementation/SKILL.md +0 -271
- package/.agents/skills/core/introducing-a-baseline-gate/SKILL.md +0 -213
- package/.agents/skills/core/knowledge-transfer/SKILL.md +0 -180
- package/.agents/skills/core/mutation-survivor-remediation/SKILL.md +0 -117
- package/.agents/skills/core/performance-optimization/SKILL.md +0 -314
- package/.agents/skills/core/planning-and-task-breakdown/SKILL.md +0 -277
- package/.agents/skills/core/property-based-testing/SKILL.md +0 -148
- package/.agents/skills/core/refactoring-discipline/SKILL.md +0 -111
- package/.agents/skills/core/shipping-and-launch/SKILL.md +0 -328
- package/.agents/skills/core/spec-driven-development/SKILL.md +0 -252
- package/.agents/skills/core/test-driven-development/SKILL.md +0 -475
- package/.agents/skills/core/using-agent-skills/SKILL.md +0 -232
- package/.agents/skills/stack/architecture/monorepo-path-strategist/SKILL.md +0 -31
- package/.agents/skills/stack/architecture/structured-output-zod/SKILL.md +0 -51
- package/.agents/skills/stack/architecture/subagent-orchestration/SKILL.md +0 -76
- package/.agents/skills/stack/backend/cloudflare-hono-architect/SKILL.md +0 -31
- package/.agents/skills/stack/backend/cloudflare-hono-architect/examples/route-template.ts +0 -33
- package/.agents/skills/stack/backend/cloudflare-queue-manager/SKILL.md +0 -31
- package/.agents/skills/stack/backend/cloudflare-workers/SKILL.md +0 -51
- package/.agents/skills/stack/backend/highlevel-crm/SKILL.md +0 -54
- package/.agents/skills/stack/backend/sqlite-drizzle-expert/SKILL.md +0 -29
- package/.agents/skills/stack/backend/sqlite-drizzle-expert/examples/schema-template.ts +0 -30
- package/.agents/skills/stack/backend/stripe-integration/SKILL.md +0 -57
- package/.agents/skills/stack/backend/stripe-integration/scripts/listen-stripe.sh +0 -9
- package/.agents/skills/stack/backend/turso-sqlite/SKILL.md +0 -48
- package/.agents/skills/stack/frontend/astro/SKILL.md +0 -62
- package/.agents/skills/stack/frontend/astro-react-island-strategist/SKILL.md +0 -30
- package/.agents/skills/stack/frontend/expo-react-native-developer/SKILL.md +0 -29
- package/.agents/skills/stack/frontend/google-analytics-v4/SKILL.md +0 -50
- package/.agents/skills/stack/frontend/tailwind-v4/SKILL.md +0 -58
- package/.agents/skills/stack/frontend/ui-accessibility-engineer/SKILL.md +0 -34
- package/.agents/skills/stack/qa/audit-accessibility/SKILL.md +0 -51
- package/.agents/skills/stack/qa/lighthouse-baseline/SKILL.md +0 -199
- package/.agents/skills/stack/security/backend-security-patterns/SKILL.md +0 -68
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Projects .agents/agents/ (and .agents/local/agents/ when present) into a flat
|
|
5
|
+
* `.claude/agents/` tree so Claude Code exposes each role definition as a
|
|
6
|
+
* role-scoped sub-agent (`subagent_type: <name>`). Exact sibling of
|
|
7
|
+
* `sync-claude-commands.js` — same header, same local-shadow policy, same
|
|
8
|
+
* orphan-reap, same prune-exempt handling for the consumer-authored
|
|
9
|
+
* `.agents/local/agents/` source. Two source directories are enumerated in
|
|
10
|
+
* order:
|
|
11
|
+
*
|
|
12
|
+
* 1. PAYLOAD_SRC — `.agents/agents/` (the installed Mandrel payload)
|
|
13
|
+
* 2. LOCAL_SRC — `.agents/local/agents/` (consumer-authored, prune-exempt)
|
|
14
|
+
*
|
|
15
|
+
* The payload directory wins on basename collision: if both sources supply
|
|
16
|
+
* `foo.md`, the payload copy is projected and the local copy is ignored with a
|
|
17
|
+
* `shadowed` warning. Because both sources are unioned into `sourceSet`, local
|
|
18
|
+
* agent defs are also protected from the orphan-reap — they survive
|
|
19
|
+
* `npm install`, `mandrel sync`, and `mandrel update` with no manual re-sync.
|
|
20
|
+
*
|
|
21
|
+
* Only top-level `.md` files project (`.claude/agents/<name>.md`). Unlike the
|
|
22
|
+
* commands sync there is no `loops/` namespace and no plugin tree to reap — the
|
|
23
|
+
* agent surface is flat.
|
|
24
|
+
*
|
|
25
|
+
* A `.claude/agents/<name>.md` runs on its **own** system prompt — it does NOT
|
|
26
|
+
* inherit the `CLAUDE.md` @-import closure — so a spawn routed to a role agent
|
|
27
|
+
* stops re-paying the always-loaded context (issue #4478). The role defs
|
|
28
|
+
* @-import `security-baseline.md` so the inviolable security MUSTs stay
|
|
29
|
+
* single-sourced.
|
|
30
|
+
*
|
|
31
|
+
* Usage: node .agents/scripts/sync-claude-agents.js
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
// cli-opt-out: top-level-await script with no main() function — runAsCli wraps an async main, which doesn't apply here.
|
|
35
|
+
import fs from 'node:fs';
|
|
36
|
+
import path from 'node:path';
|
|
37
|
+
|
|
38
|
+
import { applyHeader } from './lib/command-header.js';
|
|
39
|
+
import { Logger } from './lib/Logger.js';
|
|
40
|
+
|
|
41
|
+
// Resolve the project root from the invocation cwd — the consumer project where
|
|
42
|
+
// `.agents/` is materialized and where Claude Code loads `.claude/agents/`.
|
|
43
|
+
// It MUST NOT use `__dirname/../..`: in an npm-installed consumer this script
|
|
44
|
+
// runs from `node_modules/mandrel/.agents/scripts/`, so that climb lands on the
|
|
45
|
+
// package dir and the defs would be written *inside node_modules* rather than
|
|
46
|
+
// the consumer. Every real invocation runs with cwd at the project root
|
|
47
|
+
// (`npm run sync:agents`, the prepare hook). Tests drive fixture trees via the
|
|
48
|
+
// SYNC_CLAUDE_AGENTS_SRC/DEST overrides below.
|
|
49
|
+
const PROJECT_ROOT = process.cwd();
|
|
50
|
+
|
|
51
|
+
// Env-var overrides exist so the sync logic can be exercised against a fixture
|
|
52
|
+
// agents tree in isolation. When unset, behaviour is unchanged — the script
|
|
53
|
+
// defaults to the real agents / .claude/agents directories. SYNC_CLAUDE_AGENTS_SRC
|
|
54
|
+
// overrides the PAYLOAD source only; LOCAL_SRC is always derived from the project
|
|
55
|
+
// root so fixture tests can isolate the payload source.
|
|
56
|
+
const PAYLOAD_SRC =
|
|
57
|
+
process.env.SYNC_CLAUDE_AGENTS_SRC ??
|
|
58
|
+
path.join(PROJECT_ROOT, '.agents', 'agents');
|
|
59
|
+
const LOCAL_SRC = path.join(PROJECT_ROOT, '.agents', 'local', 'agents');
|
|
60
|
+
|
|
61
|
+
const DEST_DIR =
|
|
62
|
+
process.env.SYNC_CLAUDE_AGENTS_DEST ??
|
|
63
|
+
path.join(PROJECT_ROOT, '.claude', 'agents');
|
|
64
|
+
|
|
65
|
+
export const HEADER =
|
|
66
|
+
'<!-- AUTO-GENERATED — do not edit. Source of truth: .agents/agents/ -->\n<!-- Re-run: npm run sync:agents -->\n\n';
|
|
67
|
+
|
|
68
|
+
export const LOCAL_HEADER =
|
|
69
|
+
'<!-- AUTO-GENERATED from .agents/local/ — do not edit. Source of truth: .agents/local/agents/ -->\n<!-- Re-run: npm run sync:agents -->\n\n';
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Return true when the given directory path exists and is accessible.
|
|
73
|
+
*
|
|
74
|
+
* @param {string} dir
|
|
75
|
+
* @returns {boolean}
|
|
76
|
+
*/
|
|
77
|
+
function dirExists(dir) {
|
|
78
|
+
try {
|
|
79
|
+
return fs.statSync(dir).isDirectory();
|
|
80
|
+
} catch {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
fs.mkdirSync(DEST_DIR, { recursive: true });
|
|
86
|
+
|
|
87
|
+
// Top-level .md files project flat. Subdirectories are skipped.
|
|
88
|
+
const isTopLevelAgent = (entry) => entry.isFile() && entry.name.endsWith('.md');
|
|
89
|
+
|
|
90
|
+
// Enumerate sources: payload first, then local (if it exists). Payload wins on
|
|
91
|
+
// basename collision — a consumer must not silently shadow a core role def.
|
|
92
|
+
const SRC_DIRS = [PAYLOAD_SRC, LOCAL_SRC].filter(dirExists);
|
|
93
|
+
|
|
94
|
+
/** @type {Array<{dir: string, name: string}>} */
|
|
95
|
+
const entries = SRC_DIRS.flatMap((dir) =>
|
|
96
|
+
fs
|
|
97
|
+
.readdirSync(dir, { withFileTypes: true })
|
|
98
|
+
.filter(isTopLevelAgent)
|
|
99
|
+
.map((e) => ({ dir, name: e.name })),
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
// Collision policy: payload wins, warn on a shadowed local file.
|
|
103
|
+
const byName = new Map();
|
|
104
|
+
for (const e of entries) {
|
|
105
|
+
if (byName.has(e.name)) {
|
|
106
|
+
Logger.warn(` shadowed ${e.name} (local copy ignored; payload wins)`);
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
byName.set(e.name, e);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// sourceSet drives the orphan-reap: any existing agent def not in this set is
|
|
113
|
+
// removed.
|
|
114
|
+
const sourceSet = new Set(byName.keys());
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* List the `.md` agent defs currently on disk in the destination.
|
|
118
|
+
*
|
|
119
|
+
* @returns {string[]}
|
|
120
|
+
*/
|
|
121
|
+
function listExistingAgents() {
|
|
122
|
+
try {
|
|
123
|
+
return fs.readdirSync(DEST_DIR).filter((f) => f.endsWith('.md'));
|
|
124
|
+
} catch {
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
for (const name of listExistingAgents()) {
|
|
130
|
+
if (!sourceSet.has(name)) {
|
|
131
|
+
fs.unlinkSync(path.join(DEST_DIR, name));
|
|
132
|
+
Logger.info(` removed ${name} (no longer in .agents/agents)`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Copy each role def, injecting the auto-generated header after any leading
|
|
137
|
+
// frontmatter (so the `---` block stays on line 1 and Claude Code parses the
|
|
138
|
+
// agent's name/description). Use a distinct header comment for local-origin files.
|
|
139
|
+
let synced = 0;
|
|
140
|
+
const resolvedEntries = Array.from(byName.values());
|
|
141
|
+
await Promise.all(
|
|
142
|
+
resolvedEntries.map(async ({ dir, name }) => {
|
|
143
|
+
const isLocal = dir === LOCAL_SRC;
|
|
144
|
+
const header = isLocal ? LOCAL_HEADER : HEADER;
|
|
145
|
+
const content = await fs.promises.readFile(path.join(dir, name), 'utf8');
|
|
146
|
+
const dest = path.join(DEST_DIR, name);
|
|
147
|
+
const target = applyHeader(content, header);
|
|
148
|
+
|
|
149
|
+
// Skip write if content is already identical (avoid noisy git diffs).
|
|
150
|
+
try {
|
|
151
|
+
const existingContent = await fs.promises.readFile(dest, 'utf8');
|
|
152
|
+
if (existingContent === target) return;
|
|
153
|
+
} catch (err) {
|
|
154
|
+
if (err.code !== 'ENOENT') throw err;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
await fs.promises.writeFile(dest, target, 'utf8');
|
|
158
|
+
synced++;
|
|
159
|
+
Logger.info(` synced ${name}`);
|
|
160
|
+
}),
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
Logger.info(
|
|
164
|
+
`\n✔ ${synced} file(s) synced, ${sourceSet.size} total agents in .claude/agents/`,
|
|
165
|
+
);
|
|
@@ -13,6 +13,10 @@
|
|
|
13
13
|
import { parseArgs } from 'node:util';
|
|
14
14
|
import { resolveConfig } from './lib/config-resolver.js';
|
|
15
15
|
import { Logger } from './lib/Logger.js';
|
|
16
|
+
import {
|
|
17
|
+
enqueueLabel,
|
|
18
|
+
outboxPathFor,
|
|
19
|
+
} from './lib/orchestration/bookkeeping-outbox.js';
|
|
16
20
|
import {
|
|
17
21
|
cascadeCompletion,
|
|
18
22
|
STATE_LABELS,
|
|
@@ -33,6 +37,8 @@ if (
|
|
|
33
37
|
ticket: { type: 'string' },
|
|
34
38
|
state: { type: 'string' },
|
|
35
39
|
'remove-label': { type: 'string' },
|
|
40
|
+
buffer: { type: 'boolean' },
|
|
41
|
+
epic: { type: 'string' },
|
|
36
42
|
},
|
|
37
43
|
strict: false,
|
|
38
44
|
});
|
|
@@ -69,6 +75,31 @@ if (
|
|
|
69
75
|
return;
|
|
70
76
|
}
|
|
71
77
|
|
|
78
|
+
// Headless buffering (Epic #4476 M5): buffer an intermediate state flip to
|
|
79
|
+
// the per-Epic outbox instead of a live GitHub round-trip; finalize's
|
|
80
|
+
// bookkeeping-reconcile.js drains it once. `agent::blocked` (the HITL gate,
|
|
81
|
+
// §1.J) and `agent::done` (its cascade must run live) ALWAYS surface
|
|
82
|
+
// immediately and are never buffered. Requires --epic to locate the outbox.
|
|
83
|
+
if (
|
|
84
|
+
values.buffer &&
|
|
85
|
+
state !== STATE_LABELS.BLOCKED &&
|
|
86
|
+
state !== STATE_LABELS.DONE
|
|
87
|
+
) {
|
|
88
|
+
const epicId = Number.parseInt(values.epic ?? '', 10);
|
|
89
|
+
if (!Number.isInteger(epicId) || epicId <= 0) {
|
|
90
|
+
throw new Error('--buffer requires --epic <id>');
|
|
91
|
+
}
|
|
92
|
+
enqueueLabel({
|
|
93
|
+
outboxPath: outboxPathFor(epicId, config),
|
|
94
|
+
ticketId,
|
|
95
|
+
state,
|
|
96
|
+
});
|
|
97
|
+
Logger.info(
|
|
98
|
+
`[State-Sync] Buffered #${ticketId} → ${state} (headless; drained at finalize)`,
|
|
99
|
+
);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
72
103
|
Logger.info(
|
|
73
104
|
`[State-Sync] Transitioning ticket #${ticketId} to ${state}...`,
|
|
74
105
|
);
|
|
@@ -124,6 +124,66 @@ export function readLedgerLastEvents(ledgerPath) {
|
|
|
124
124
|
return out;
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
/**
|
|
128
|
+
* Single-delivery analogue of {@link readLedgerLastEvents} (Epic #4476, M5).
|
|
129
|
+
* A single-delivery run has no Story fan-out; the whole Epic is one long
|
|
130
|
+
* guarded session walking the Delivery Slicing table, and its liveness signal
|
|
131
|
+
* is `slice.heartbeat` (emitted off the token stream by the PostToolUse hook).
|
|
132
|
+
* This scans the ledger for `slice.*` events, treats a slice carrying a
|
|
133
|
+
* `slice.start` without a matching `slice.end` as in-flight, and returns each
|
|
134
|
+
* in-flight slice's most recent event timestamp — the same
|
|
135
|
+
* started-without-ended shape the Story reader uses, keyed by the string
|
|
136
|
+
* `sliceId`. Any `slice.*` event (notably `slice.heartbeat`) refreshes the
|
|
137
|
+
* timestamp.
|
|
138
|
+
*
|
|
139
|
+
* Pure helper — exported for unit tests.
|
|
140
|
+
*
|
|
141
|
+
* @param {string} ledgerPath Absolute path to `lifecycle.ndjson`.
|
|
142
|
+
* @returns {Map<string, string>} `sliceId → ISO-8601 timestamp`. Empty when
|
|
143
|
+
* the ledger is missing, unreadable, or carries no in-flight slices.
|
|
144
|
+
*/
|
|
145
|
+
export function readLedgerSliceEvents(ledgerPath) {
|
|
146
|
+
if (!ledgerPath || !existsSync(ledgerPath)) return new Map();
|
|
147
|
+
let raw;
|
|
148
|
+
try {
|
|
149
|
+
raw = readFileSync(ledgerPath, 'utf8');
|
|
150
|
+
} catch {
|
|
151
|
+
return new Map();
|
|
152
|
+
}
|
|
153
|
+
if (!raw) return new Map();
|
|
154
|
+
const started = new Set();
|
|
155
|
+
const ended = new Set();
|
|
156
|
+
const lastTs = new Map();
|
|
157
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
158
|
+
if (!line) continue;
|
|
159
|
+
let record;
|
|
160
|
+
try {
|
|
161
|
+
record = JSON.parse(line);
|
|
162
|
+
} catch {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (!record || record.kind !== 'emitted') continue;
|
|
166
|
+
const event = typeof record.event === 'string' ? record.event : '';
|
|
167
|
+
if (!event.startsWith('slice.')) continue;
|
|
168
|
+
const sliceId = record.payload?.sliceId;
|
|
169
|
+
if (typeof sliceId !== 'string' || sliceId.length === 0) continue;
|
|
170
|
+
if (event === 'slice.start') started.add(sliceId);
|
|
171
|
+
else if (event === 'slice.end') ended.add(sliceId);
|
|
172
|
+
const ts = typeof record.ts === 'string' ? record.ts : null;
|
|
173
|
+
if (ts) {
|
|
174
|
+
const prior = lastTs.get(sliceId);
|
|
175
|
+
if (!prior || prior < ts) lastTs.set(sliceId, ts);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
const out = new Map();
|
|
179
|
+
for (const id of started) {
|
|
180
|
+
if (ended.has(id)) continue;
|
|
181
|
+
const ts = lastTs.get(id);
|
|
182
|
+
if (ts) out.set(id, ts);
|
|
183
|
+
}
|
|
184
|
+
return out;
|
|
185
|
+
}
|
|
186
|
+
|
|
127
187
|
/**
|
|
128
188
|
* Deterministic branch-activity resolver (Story #3900). Returns the epoch-ms
|
|
129
189
|
* timestamp of the most recent commit on `story-<storyId>` via
|
|
@@ -139,18 +199,25 @@ export function readLedgerLastEvents(ledgerPath) {
|
|
|
139
199
|
* and injectable so unit tests never spawn a subprocess.
|
|
140
200
|
*
|
|
141
201
|
* @param {number} storyId
|
|
142
|
-
* @param {{ cwd?: string, exec?: typeof execFileSync }} [deps]
|
|
202
|
+
* @param {{ cwd?: string, exec?: typeof execFileSync, branch?: string }} [deps]
|
|
203
|
+
* `branch` overrides the default `story-<storyId>` ref — the single-delivery
|
|
204
|
+
* watchdog passes `epic-<id>` (Epic #4476, M5) to consult the one
|
|
205
|
+
* integration branch instead of a per-Story branch.
|
|
143
206
|
* @returns {number|null} epoch-ms of the last commit, or null.
|
|
144
207
|
*/
|
|
145
208
|
export function branchLastCommitMs(storyId, deps = {}) {
|
|
146
209
|
const exec = deps.exec ?? execFileSync;
|
|
147
210
|
const cwd = deps.cwd ?? process.cwd();
|
|
211
|
+
const branch =
|
|
212
|
+
typeof deps.branch === 'string' && deps.branch
|
|
213
|
+
? deps.branch
|
|
214
|
+
: `story-${storyId}`;
|
|
148
215
|
try {
|
|
149
|
-
const out = exec(
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
).trim();
|
|
216
|
+
const out = exec('git', ['log', '-1', '--format=%cI', branch, '--'], {
|
|
217
|
+
cwd,
|
|
218
|
+
encoding: 'utf8',
|
|
219
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
220
|
+
}).trim();
|
|
154
221
|
if (!out) return null;
|
|
155
222
|
const ms = Date.parse(out);
|
|
156
223
|
return Number.isFinite(ms) ? ms : null;
|
|
@@ -180,13 +247,26 @@ export function branchLastCommitMs(storyId, deps = {}) {
|
|
|
180
247
|
* @param {(storyId: number) => (number|null)} [args.branchActivity]
|
|
181
248
|
* Resolver for the last commit epoch-ms on `story-<id>`. Defaults to
|
|
182
249
|
* {@link branchLastCommitMs}. Injected for tests.
|
|
250
|
+
* @param {Map<string, string>} [args.sliceEvents] `sliceId → ISO-8601 ts`
|
|
251
|
+
* from {@link readLedgerSliceEvents} (Epic #4476, M5). Single-delivery has
|
|
252
|
+
* no Story fan-out; its liveness signal is `slice.heartbeat`. When
|
|
253
|
+
* non-empty, in-flight slices are surfaced under the additive
|
|
254
|
+
* `inFlightSlices` / `stalledSlices` fields — the story fields are
|
|
255
|
+
* untouched, so a fan-out run's envelope is byte-for-byte unchanged.
|
|
256
|
+
* @param {(epicId: number) => (number|null)} [args.epicBranchActivity]
|
|
257
|
+
* Resolver for the last commit epoch-ms on the single-delivery integration
|
|
258
|
+
* branch `epic-<id>` — the deterministic forward-progress signal for the
|
|
259
|
+
* one long session, mirroring the per-Story branch check. Injected for
|
|
260
|
+
* tests.
|
|
183
261
|
* @returns {{
|
|
184
262
|
* kind: 'wave-stall',
|
|
185
263
|
* epicId: number,
|
|
186
264
|
* thresholdMinutes: number,
|
|
187
265
|
* checkedAt: string,
|
|
188
266
|
* stalled: Array<{storyId: number, lastEventAt: string, idleMinutes: number}>,
|
|
189
|
-
* inFlight: number[]
|
|
267
|
+
* inFlight: number[],
|
|
268
|
+
* stalledSlices: Array<{sliceId: string, lastEventAt: string, idleMinutes: number}>,
|
|
269
|
+
* inFlightSlices: string[]
|
|
190
270
|
* }}
|
|
191
271
|
*/
|
|
192
272
|
export function buildWaveStallEnvelope({
|
|
@@ -195,6 +275,9 @@ export function buildWaveStallEnvelope({
|
|
|
195
275
|
lastEvents,
|
|
196
276
|
now,
|
|
197
277
|
branchActivity = branchLastCommitMs,
|
|
278
|
+
sliceEvents = new Map(),
|
|
279
|
+
epicBranchActivity = (eid) =>
|
|
280
|
+
branchLastCommitMs(0, { branch: `epic-${eid}` }),
|
|
198
281
|
}) {
|
|
199
282
|
const checkedAt = now.toISOString();
|
|
200
283
|
const thresholdMs = thresholdMinutes * 60 * 1000;
|
|
@@ -227,6 +310,37 @@ export function buildWaveStallEnvelope({
|
|
|
227
310
|
}
|
|
228
311
|
inFlight.sort((a, b) => a - b);
|
|
229
312
|
stalled.sort((a, b) => a.storyId - b.storyId);
|
|
313
|
+
|
|
314
|
+
// Single-delivery slice liveness (additive). A slice that has gone silent
|
|
315
|
+
// past the threshold is a stall UNLESS the `epic-<id>` branch carries a
|
|
316
|
+
// recent commit — the same forward-progress carve-out the Story path uses.
|
|
317
|
+
const stalledSlices = [];
|
|
318
|
+
const inFlightSlices = [];
|
|
319
|
+
for (const [sliceId, ts] of sliceEvents) {
|
|
320
|
+
inFlightSlices.push(sliceId);
|
|
321
|
+
const eventMs = Date.parse(ts);
|
|
322
|
+
if (!Number.isFinite(eventMs)) continue;
|
|
323
|
+
const idleMs = now.getTime() - eventMs;
|
|
324
|
+
if (idleMs < thresholdMs) continue;
|
|
325
|
+
|
|
326
|
+
const commitMs = epicBranchActivity(epicId);
|
|
327
|
+
if (
|
|
328
|
+
typeof commitMs === 'number' &&
|
|
329
|
+
Number.isFinite(commitMs) &&
|
|
330
|
+
now.getTime() - commitMs < thresholdMs
|
|
331
|
+
) {
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
stalledSlices.push({
|
|
336
|
+
sliceId,
|
|
337
|
+
lastEventAt: ts,
|
|
338
|
+
idleMinutes: Math.floor(idleMs / 60000),
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
inFlightSlices.sort();
|
|
342
|
+
stalledSlices.sort((a, b) => a.sliceId.localeCompare(b.sliceId));
|
|
343
|
+
|
|
230
344
|
return {
|
|
231
345
|
kind: 'wave-stall',
|
|
232
346
|
epicId,
|
|
@@ -234,6 +348,8 @@ export function buildWaveStallEnvelope({
|
|
|
234
348
|
checkedAt,
|
|
235
349
|
stalled,
|
|
236
350
|
inFlight,
|
|
351
|
+
stalledSlices,
|
|
352
|
+
inFlightSlices,
|
|
237
353
|
};
|
|
238
354
|
}
|
|
239
355
|
|
|
@@ -251,7 +367,13 @@ export function buildWaveStallEnvelope({
|
|
|
251
367
|
* @param {(storyId: number) => (number|null)} [args.branchActivity]
|
|
252
368
|
* Branch-liveness resolver; defaults to {@link branchLastCommitMs}.
|
|
253
369
|
* Injected for tests so the watchdog never spawns git.
|
|
254
|
-
* @
|
|
370
|
+
* @param {(epicId: number) => (number|null)} [args.epicBranchActivity]
|
|
371
|
+
* Single-delivery integration-branch (`epic-<id>`) liveness resolver;
|
|
372
|
+
* injected for tests. Defaults to {@link branchLastCommitMs} with the
|
|
373
|
+
* `epic-<id>` ref.
|
|
374
|
+
* @returns {{envelope: object, stalledCount: number}} `stalledCount` counts
|
|
375
|
+
* both stalled Stories (fan-out) and stalled slices (single delivery), so a
|
|
376
|
+
* dead single-delivery session still exits the CLI non-zero.
|
|
255
377
|
*/
|
|
256
378
|
export function runCheckIdle({
|
|
257
379
|
epicId,
|
|
@@ -260,6 +382,7 @@ export function runCheckIdle({
|
|
|
260
382
|
now = new Date(),
|
|
261
383
|
config,
|
|
262
384
|
branchActivity = branchLastCommitMs,
|
|
385
|
+
epicBranchActivity,
|
|
263
386
|
}) {
|
|
264
387
|
if (!Number.isInteger(epicId) || epicId <= 0) {
|
|
265
388
|
throw new TypeError('runCheckIdle: epicId must be a positive integer');
|
|
@@ -271,14 +394,20 @@ export function runCheckIdle({
|
|
|
271
394
|
}
|
|
272
395
|
const resolvedPath = ledgerPath ?? epicLedgerPath(epicId, config);
|
|
273
396
|
const lastEvents = readLedgerLastEvents(resolvedPath);
|
|
397
|
+
const sliceEvents = readLedgerSliceEvents(resolvedPath);
|
|
274
398
|
const envelope = buildWaveStallEnvelope({
|
|
275
399
|
epicId,
|
|
276
400
|
thresholdMinutes,
|
|
277
401
|
lastEvents,
|
|
278
402
|
now,
|
|
279
403
|
branchActivity,
|
|
404
|
+
sliceEvents,
|
|
405
|
+
...(epicBranchActivity ? { epicBranchActivity } : {}),
|
|
280
406
|
});
|
|
281
|
-
return {
|
|
407
|
+
return {
|
|
408
|
+
envelope,
|
|
409
|
+
stalledCount: envelope.stalled.length + envelope.stalledSlices.length,
|
|
410
|
+
};
|
|
282
411
|
}
|
|
283
412
|
|
|
284
413
|
async function main(argv) {
|
|
@@ -19,7 +19,7 @@ description:
|
|
|
19
19
|
- Define the contract before implementing — interfaces are the spec; implementation follows.
|
|
20
20
|
- Validate at system **boundaries** (API routes, form handlers, env-var loaders, third-party responses) using a strict schema. After validation, internal code trusts the types; do not re-validate between internal functions.
|
|
21
21
|
- Treat third-party API responses as untrusted data — validate shape and content before using them in any decision, render, or logic path.
|
|
22
|
-
- Prefer **addition over modification**: extend interfaces with optional fields rather than changing existing types or removing fields
|
|
22
|
+
- Prefer **addition over modification**: extend interfaces with optional fields rather than changing existing types or removing fields. When removal is unavoidable, use an expand–contract migration — ship the replacement, migrate consumers, then remove the old surface in a later release.
|
|
23
23
|
- Follow REST resource conventions (`GET/POST/PATCH/DELETE /resource`, sub-resources at `/resource/:id/child`) and paginate every list endpoint with `page` + `pageSize` query params and a `pagination` envelope.
|
|
24
24
|
- Security input-validation and test-tier MUSTs come from `.agents/rules/security-baseline.md` and `.agents/rules/testing-standards.md` respectively — apply both, and never put DB/wire-shape assertions outside the contract tier.
|
|
25
25
|
|
|
@@ -55,8 +55,10 @@ ordering — becomes a de facto contract once users depend on it. Implications:
|
|
|
55
55
|
potential commitment.
|
|
56
56
|
- **Don't leak implementation details.** If users can observe it, they will
|
|
57
57
|
depend on it.
|
|
58
|
-
- **Plan for deprecation at design time.**
|
|
59
|
-
|
|
58
|
+
- **Plan for deprecation at design time.** Remove things users depend on via
|
|
59
|
+
expand–contract: add the replacement, migrate consumers behind a deprecation
|
|
60
|
+
window, then delete the old surface — never break a published contract in one
|
|
61
|
+
step.
|
|
60
62
|
- **Tests are not enough.** Even with perfect contract tests, "safe" changes
|
|
61
63
|
can break real users who depend on undocumented behavior.
|
|
62
64
|
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: code-review-and-quality
|
|
3
3
|
description:
|
|
4
|
-
Conducts multi-axis code review
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
Conducts multi-axis code review and runs the disciplined post-green refactor
|
|
5
|
+
pass. Use before merging any change, when reviewing code written by yourself,
|
|
6
|
+
another agent, or a human, or when the opt-in `delivery.refactorStage`
|
|
7
|
+
checkpoint asks for a behaviour-preserving CRAP/duplication pass after the
|
|
8
|
+
suite is green.
|
|
8
9
|
---
|
|
9
10
|
|
|
10
11
|
# Code Review and Quality
|
|
@@ -18,9 +19,11 @@ description:
|
|
|
18
19
|
- Reject "clever" code in favour of the boring, obvious solution. Abstractions MUST earn their complexity (no generalizing before the third use case).
|
|
19
20
|
- Flag dead code artifacts (`_unused` vars, backwards-compat shims, `// removed` comments) and require their removal before merge.
|
|
20
21
|
- Defer to `.agents/rules/security-baseline.md` and the `security-and-hardening` skill for security review; explicit checks include input validation, no hardcoded secrets, parameterized queries, encoded output, authn+authz, and treating external data as untrusted.
|
|
21
|
-
-
|
|
22
|
+
- Review performance on the hot path only, and measure before optimizing; explicit checks include no N+1 queries, no unbounded fetches, no blocking sync work, no obviously oversized bundles.
|
|
22
23
|
- Disallow scope creep in a PR: drive-by cleanups, adjacent refactors, and "while I'm here" edits should be split into a separate change.
|
|
23
24
|
- Bug-fix reviews cover **both** the fix and the regression test; a fix without a failing-then-passing test is not approvable.
|
|
25
|
+
- **Refactoring is post-green and behaviour-preserving.** Run the refactor pass only when the suite is already green and gates pass; inputs, outputs, side effects, error semantics, and ordering MUST be identical before and after, and existing tests MUST keep passing **without modification** (if a test had to change, behaviour changed — revert).
|
|
26
|
+
- **Lower CRAP by lowering complexity, and remove duplication at the root.** Target the highest-CRAP well-covered functions and the largest verbatim duplications by measurement (`check-baselines.js`), not by smell; extract one well-named helper rather than leaving near-copies. CRAP must not rise and maintainability must not fall on any touched file, and no gate, floor, or threshold may be retuned to make the pass "succeed".
|
|
24
27
|
|
|
25
28
|
## Overview
|
|
26
29
|
|
|
@@ -105,8 +108,8 @@ introduce vulnerabilities?
|
|
|
105
108
|
|
|
106
109
|
### 5. Performance
|
|
107
110
|
|
|
108
|
-
|
|
109
|
-
|
|
111
|
+
Measure before optimizing, and only on the hot path. Does the change introduce
|
|
112
|
+
performance problems?
|
|
110
113
|
|
|
111
114
|
- Any N+1 query patterns?
|
|
112
115
|
- Any unbounded loops or unconstrained data fetching?
|
|
@@ -409,6 +412,50 @@ Every dependency is a liability.
|
|
|
409
412
|
optional
|
|
410
413
|
- Accepting "I'll fix it later" — it never happens
|
|
411
414
|
|
|
415
|
+
## Post-Green Refactor Pass
|
|
416
|
+
|
|
417
|
+
Review's sibling discipline is the **behaviour-preserving refactor** that drives
|
|
418
|
+
CRAP (Change Risk Anti-Patterns) and duplication down _after_ the code is green
|
|
419
|
+
— the pass wired in as the opt-in `delivery.refactorStage` checkpoint. It
|
|
420
|
+
optimises for the measured CRAP and duplication axes the baselines ratchet, not
|
|
421
|
+
for subjective readability, and it never runs from red.
|
|
422
|
+
|
|
423
|
+
**When to run:** a baseline report flags a high-CRAP function that is already
|
|
424
|
+
well-covered (so the lever is complexity, not coverage); the same logic is
|
|
425
|
+
duplicated across two or more files and the copies are drifting; or the
|
|
426
|
+
`delivery.refactorStage` checkpoint fires after gates pass and before a Story
|
|
427
|
+
closes. **When NOT to:** before tests are green (write/fix tests first), or when
|
|
428
|
+
a function's CRAP is high because it is _uncovered_ — that is a missing-test
|
|
429
|
+
problem, so drive it with the TDD cycle in
|
|
430
|
+
[`.agents/rules/testing-standards.md`](../../../rules/testing-standards.md).
|
|
431
|
+
|
|
432
|
+
```text
|
|
433
|
+
Confirm green + baseline → pick highest-CRAP / largest dup
|
|
434
|
+
▲ │
|
|
435
|
+
│ ▼
|
|
436
|
+
re-run gates, confirm ◄── refactor ◄── comprehend the target
|
|
437
|
+
CRAP↓ / maint↑, tests pass (one change) (call sites + tests)
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
1. **Baseline.** Confirm green and capture current CRAP and maintainability
|
|
441
|
+
(`node .agents/scripts/check-baselines.js`) — your do-no-harm reference.
|
|
442
|
+
2. **Target by measurement.** Pick the highest-CRAP well-covered function, or
|
|
443
|
+
the largest verbatim duplication. Work worst-first.
|
|
444
|
+
3. **Comprehend before you touch (Chesterton's Fence).** Read the call sites and
|
|
445
|
+
the tests that pin the behaviour first; an "unnecessary" abstraction may
|
|
446
|
+
exist for testability or extensibility.
|
|
447
|
+
4. **Refactor one step.** Extract a focused function, flatten nesting into guard
|
|
448
|
+
clauses, replace a nested ternary / boolean-flag param, or extract a single
|
|
449
|
+
shared helper for the duplication (dedup the _behaviour_, not just the text —
|
|
450
|
+
look-alike blocks that encode different responsibilities stay separate).
|
|
451
|
+
5. **Verify.** Re-run the affected tests (unmodified) and the baselines. CRAP
|
|
452
|
+
must not rise, maintainability must not fall, tests must pass. If a test
|
|
453
|
+
needed editing, revert — behaviour changed.
|
|
454
|
+
|
|
455
|
+
Keep each refactor an isolated, reviewable commit separate from feature or fix
|
|
456
|
+
work, refactor only the targeted functions/duplications (no drive-by rewrites),
|
|
457
|
+
and leave no dead code behind (unused imports, orphaned helpers).
|
|
458
|
+
|
|
412
459
|
## Verification
|
|
413
460
|
|
|
414
461
|
After review is complete:
|
|
@@ -419,3 +466,12 @@ After review is complete:
|
|
|
419
466
|
- [ ] Tests pass
|
|
420
467
|
- [ ] Build succeeds
|
|
421
468
|
- [ ] The verification story is documented (what changed, how it was verified)
|
|
469
|
+
|
|
470
|
+
After a post-green refactor pass:
|
|
471
|
+
|
|
472
|
+
- [ ] The suite was green before and after, with **no test modifications**
|
|
473
|
+
- [ ] CRAP did not rise and maintainability did not fall on any touched file
|
|
474
|
+
(baselines re-run and compared)
|
|
475
|
+
- [ ] No gate, floor, or coverage threshold was loosened
|
|
476
|
+
- [ ] Duplication was removed at the root (one helper), not just locally patched
|
|
477
|
+
- [ ] Each refactor is an isolated commit, separate from feature or fix work
|
|
@@ -15,7 +15,7 @@ description:
|
|
|
15
15
|
- Follow the triage checklist in order — **Reproduce → Localize → Diagnose → Fix → Verify → Guard** — and never skip steps.
|
|
16
16
|
- A bug you cannot reproduce reliably is a bug you cannot fix with confidence. Invest in reproduction before patching.
|
|
17
17
|
- Fix the **root cause**, not the symptom. Suppressing an error, swallowing an exception, or stubbing an assertion is not a fix.
|
|
18
|
-
- Every bug fix ships with a **failing-then-passing regression test** (the Prove-It Pattern
|
|
18
|
+
- Every bug fix ships with a **failing-then-passing regression test** (the Prove-It Pattern in `.agents/rules/testing-standards.md`). A fix without a guard test is incomplete.
|
|
19
19
|
- Apply the **Anti-Thrashing** rule: if you have applied the same kind of fix more than once and the failure mode hasn't changed, the diagnosis is wrong — re-plan.
|
|
20
20
|
- For non-reproducible bugs, classify them (timing / environment / state / random), add targeted instrumentation, and document conditions instead of chasing in the dark.
|
|
21
21
|
- Bisect history with `git bisect` (or equivalent) when "something used to work" and you cannot localize from the diff.
|
|
@@ -4,7 +4,7 @@ description: >-
|
|
|
4
4
|
Run a holistic, pre-persist consolidation pass over the draft Story
|
|
5
5
|
ticket array an Epic's decompose phase produced. Use during Phase 8 of
|
|
6
6
|
`/plan`, after `epic-plan-decompose-author` writes
|
|
7
|
-
`temp/epic-<Epic_ID>/tickets.json` and before `
|
|
7
|
+
`temp/epic-<Epic_ID>/tickets.json` and before `plan-persist.js`
|
|
8
8
|
validates and persists it. Reconciles the draft against the Tech Spec
|
|
9
9
|
"Delivery Slicing" ceiling via scope-preserving operations only.
|
|
10
10
|
allowed_tools:
|
|
@@ -48,7 +48,7 @@ critic cannot grade its own homework.
|
|
|
48
48
|
`/plan` Phase 8, as the **8.3 — Holistic Consolidation** sub-step:
|
|
49
49
|
immediately after `epic-plan-decompose-author` writes
|
|
50
50
|
`temp/epic-<Epic_ID>/tickets.json` and **before**
|
|
51
|
-
`
|
|
51
|
+
`plan-persist.js --tickets …` validates and persists. The pass operates
|
|
52
52
|
on the temp artifact so the holistic adjustment happens before the GitHub
|
|
53
53
|
write; the deterministic validator runs *after* it, so the critic can never
|
|
54
54
|
emit a plan the validator would reject.
|
|
@@ -61,7 +61,7 @@ passing the Epic ID as the Skill argument. The Skill itself reads:
|
|
|
61
61
|
- `temp/epic-<Epic_ID>/tickets.json` — the **draft** Story array the
|
|
62
62
|
`epic-plan-decompose-author` Skill wrote. This is the consolidation input.
|
|
63
63
|
- `temp/epic-<Epic_ID>/decomposer-context.json` — the authoring envelope
|
|
64
|
-
emitted by `
|
|
64
|
+
emitted by `plan-context.js --epic <Epic_ID>`. Read `epicBody`
|
|
65
65
|
from it — the sectioned Epic body carrying the folded Tech Spec
|
|
66
66
|
sections (there is no separate `techSpec` key — Story #4324). The
|
|
67
67
|
**"Delivery Slicing"**
|
|
@@ -74,7 +74,7 @@ passing the Epic ID as the Skill argument. The Skill itself reads:
|
|
|
74
74
|
- `temp/epic-<Epic_ID>/tickets.json` — the **consolidated** array, overwriting
|
|
75
75
|
the draft. Same schema as the author skill emits (flat Story array; Stories
|
|
76
76
|
carry top-level `acceptance[]` / `verify[]`; `body` is a serialized string).
|
|
77
|
-
The downstream `
|
|
77
|
+
The downstream `plan-persist.js --tickets …` validator is the final
|
|
78
78
|
gate — author for its rules, not for "looks right."
|
|
79
79
|
- `temp/epic-<Epic_ID>/consolidation-report.md` — a human-readable
|
|
80
80
|
rationale + before/after diff. This is the artifact the workflow shows the
|
|
@@ -152,7 +152,7 @@ so the operator sees the coarsening at the Phase 8.3 advisory diff.
|
|
|
152
152
|
|
|
153
153
|
Return control. The workflow shows the operator the consolidation report at the
|
|
154
154
|
HITL diff gate; on approval it runs
|
|
155
|
-
`node .agents/scripts/
|
|
155
|
+
`node .agents/scripts/plan-persist.js --epic <Epic_ID> --tickets
|
|
156
156
|
temp/epic-<Epic_ID>/tickets.json`, which validates the consolidated array,
|
|
157
157
|
persists the hierarchy, and flips the Epic to `agent::ready`.
|
|
158
158
|
|