cleargate 0.14.0 → 0.15.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/CHANGELOG.md +16 -0
- package/dist/MANIFEST.json +71 -15
- package/dist/admin-api/index.cjs +0 -1
- package/dist/admin-api/index.js +1 -2
- package/dist/auth/factory.cjs +0 -1
- package/dist/auth/factory.js +2 -3
- package/dist/auth/require-token.cjs +0 -1
- package/dist/auth/require-token.js +1 -2
- package/dist/auth/token-store.cjs +0 -1
- package/dist/auth/token-store.js +1 -2
- package/dist/{bootstrap-root-QKSA5V75.js → bootstrap-root-2H5HVTCC.js} +1 -2
- package/dist/{chunk-PDE37WFQ.js → chunk-A7MSQUU7.js} +2 -3
- package/dist/{chunk-BTSZOEWC.js → chunk-P6KEDAK2.js} +0 -1
- package/dist/{chunk-E3X7IE5E.js → chunk-PY6FHGV5.js} +1 -2
- package/dist/{chunk-5DI2Z3C2.js → chunk-Y53ZZYYU.js} +1 -2
- package/dist/cli.cjs +1564 -1414
- package/dist/cli.js +1514 -1364
- package/dist/lib/ledger.cjs +0 -1
- package/dist/lib/ledger.js +1 -2
- package/dist/lib/lifecycle-reconcile.cjs +0 -1
- package/dist/lib/lifecycle-reconcile.js +2 -3
- package/dist/{whoami-EANGN46Z.js → whoami-JKQQPABQ.js} +3 -4
- package/package.json +4 -3
- package/templates/cleargate-planning/.claude/agents/architect.md +4 -2
- package/templates/cleargate-planning/.claude/agents/developer.md +4 -11
- package/templates/cleargate-planning/.claude/agents/qa.md +14 -6
- package/templates/cleargate-planning/.claude/hooks/pending-task-sentinel.sh +2 -2
- package/templates/cleargate-planning/.claude/skills/sprint-execution/SKILL.md +19 -1
- package/templates/cleargate-planning/.cleargate/config.example.yml +16 -0
- package/templates/cleargate-planning/.cleargate/scripts/close_sprint.deferred-verify.red.node.test.ts +245 -0
- package/templates/cleargate-planning/.cleargate/scripts/close_sprint.mjs +227 -0
- package/templates/cleargate-planning/.cleargate/scripts/gate-checks.json +5 -4
- package/templates/cleargate-planning/.cleargate/scripts/init_sprint.mjs +75 -2
- package/templates/cleargate-planning/.cleargate/scripts/pre_gate_common.sh +48 -0
- package/templates/cleargate-planning/.cleargate/scripts/pre_gate_runner.sh +57 -1
- package/templates/cleargate-planning/.cleargate/scripts/provision_worktree_config.sh +155 -0
- package/templates/cleargate-planning/.cleargate/scripts/qa_red_lint.mjs +380 -0
- package/templates/cleargate-planning/.cleargate/scripts/run_script.sh +34 -1
- package/templates/cleargate-planning/.cleargate/scripts/test/cr077_eviction.red.sh +113 -0
- package/templates/cleargate-planning/.cleargate/scripts/test/cr078_init.test.sh +309 -0
- package/templates/cleargate-planning/.cleargate/scripts/test/cr079_provision.red.sh +262 -0
- package/templates/cleargate-planning/.cleargate/scripts/test/cr080_wrapper.test.sh +177 -0
- package/templates/cleargate-planning/.cleargate/scripts/test/cr081_qa_red_lint.red.sh +348 -0
- package/templates/cleargate-planning/.cleargate/sprint-runs/_off-sprint/.session-totals.json +1 -0
- package/templates/cleargate-planning/.cleargate/sprint-runs/_off-sprint/token-ledger.jsonl +27 -0
- package/templates/cleargate-planning/.cleargate/templates/sprint_context.md +17 -0
- package/templates/cleargate-planning/.cleargate/templates/story.md +1 -0
- package/templates/cleargate-planning/MANIFEST.json +71 -15
- package/dist/admin-api/index.cjs.map +0 -1
- package/dist/admin-api/index.js.map +0 -1
- package/dist/auth/factory.cjs.map +0 -1
- package/dist/auth/factory.js.map +0 -1
- package/dist/auth/require-token.cjs.map +0 -1
- package/dist/auth/require-token.js.map +0 -1
- package/dist/auth/token-store.cjs.map +0 -1
- package/dist/auth/token-store.js.map +0 -1
- package/dist/bootstrap-root-QKSA5V75.js.map +0 -1
- package/dist/chunk-5DI2Z3C2.js.map +0 -1
- package/dist/chunk-BTSZOEWC.js.map +0 -1
- package/dist/chunk-E3X7IE5E.js.map +0 -1
- package/dist/chunk-PDE37WFQ.js.map +0 -1
- package/dist/cli.cjs.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/lib/ledger.cjs.map +0 -1
- package/dist/lib/ledger.js.map +0 -1
- package/dist/lib/lifecycle-reconcile.cjs.map +0 -1
- package/dist/lib/lifecycle-reconcile.js.map +0 -1
- package/dist/templates/cleargate-planning/.claude/agents/architect-reader.md +0 -61
- package/dist/templates/cleargate-planning/.claude/agents/architect-synth.md +0 -124
- package/dist/templates/cleargate-planning/.claude/agents/architect.md +0 -230
- package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-contradict.md +0 -108
- package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-ingest.md +0 -194
- package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-lint.md +0 -261
- package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-query.md +0 -143
- package/dist/templates/cleargate-planning/.claude/agents/developer.md +0 -185
- package/dist/templates/cleargate-planning/.claude/agents/devops.md +0 -257
- package/dist/templates/cleargate-planning/.claude/agents/qa.md +0 -171
- package/dist/templates/cleargate-planning/.claude/agents/reporter.md +0 -274
- package/dist/templates/cleargate-planning/.claude/hooks/pending-task-sentinel.sh +0 -209
- package/dist/templates/cleargate-planning/.claude/hooks/pre-commit-surface-gate.sh +0 -33
- package/dist/templates/cleargate-planning/.claude/hooks/pre-commit-test-ratchet.sh +0 -58
- package/dist/templates/cleargate-planning/.claude/hooks/pre-commit.sh +0 -19
- package/dist/templates/cleargate-planning/.claude/hooks/pre-edit-gate.sh +0 -162
- package/dist/templates/cleargate-planning/.claude/hooks/pre-tool-use-autonomy.sh +0 -58
- package/dist/templates/cleargate-planning/.claude/hooks/pre-tool-use-task.sh +0 -148
- package/dist/templates/cleargate-planning/.claude/hooks/session-start.sh +0 -75
- package/dist/templates/cleargate-planning/.claude/hooks/stamp-and-gate.sh +0 -43
- package/dist/templates/cleargate-planning/.claude/hooks/token-ledger.sh +0 -590
- package/dist/templates/cleargate-planning/.claude/settings.json +0 -68
- package/dist/templates/cleargate-planning/.claude/skills/flashcard/SKILL.md +0 -102
- package/dist/templates/cleargate-planning/.claude/skills/sprint-execution/SKILL.md +0 -742
- package/dist/templates/cleargate-planning/.cleargate/FLASHCARD.md +0 -7
- package/dist/templates/cleargate-planning/.cleargate/config.example.yml +0 -67
- package/dist/templates/cleargate-planning/.cleargate/config.yml +0 -18
- package/dist/templates/cleargate-planning/.cleargate/delivery/archive/.gitkeep +0 -0
- package/dist/templates/cleargate-planning/.cleargate/delivery/pending-sync/.gitkeep +0 -0
- package/dist/templates/cleargate-planning/.cleargate/knowledge/cleargate-enforcement.md +0 -551
- package/dist/templates/cleargate-planning/.cleargate/knowledge/cleargate-protocol.md +0 -878
- package/dist/templates/cleargate-planning/.cleargate/knowledge/mid-sprint-triage-rubric.md +0 -160
- package/dist/templates/cleargate-planning/.cleargate/knowledge/readiness-gates.md +0 -213
- package/dist/templates/cleargate-planning/.cleargate/knowledge/sprint-closeout-checklist.md +0 -71
- package/dist/templates/cleargate-planning/.cleargate/scripts/_migrate-schema-v3.mjs +0 -120
- package/dist/templates/cleargate-planning/.cleargate/scripts/assert_story_files.mjs +0 -265
- package/dist/templates/cleargate-planning/.cleargate/scripts/close_sprint.mjs +0 -1012
- package/dist/templates/cleargate-planning/.cleargate/scripts/collision_surface.sh +0 -114
- package/dist/templates/cleargate-planning/.cleargate/scripts/constants.mjs +0 -62
- package/dist/templates/cleargate-planning/.cleargate/scripts/dedupe_frontmatter.mjs +0 -219
- package/dist/templates/cleargate-planning/.cleargate/scripts/file_surface_diff.sh +0 -320
- package/dist/templates/cleargate-planning/.cleargate/scripts/gate-checks.json +0 -15
- package/dist/templates/cleargate-planning/.cleargate/scripts/init_gate_config.sh +0 -38
- package/dist/templates/cleargate-planning/.cleargate/scripts/init_sprint.mjs +0 -240
- package/dist/templates/cleargate-planning/.cleargate/scripts/launch_wave.mjs +0 -341
- package/dist/templates/cleargate-planning/.cleargate/scripts/lib/report-filename.mjs +0 -54
- package/dist/templates/cleargate-planning/.cleargate/scripts/pre_gate_common.sh +0 -206
- package/dist/templates/cleargate-planning/.cleargate/scripts/pre_gate_runner.sh +0 -371
- package/dist/templates/cleargate-planning/.cleargate/scripts/prefill_report.mjs +0 -280
- package/dist/templates/cleargate-planning/.cleargate/scripts/prep_doc_refresh.mjs +0 -378
- package/dist/templates/cleargate-planning/.cleargate/scripts/prep_qa_context.mjs +0 -888
- package/dist/templates/cleargate-planning/.cleargate/scripts/run_script.sh +0 -209
- package/dist/templates/cleargate-planning/.cleargate/scripts/sprint_trends.mjs +0 -71
- package/dist/templates/cleargate-planning/.cleargate/scripts/state.schema.json +0 -127
- package/dist/templates/cleargate-planning/.cleargate/scripts/suggest_improvements.mjs +0 -717
- package/dist/templates/cleargate-planning/.cleargate/scripts/surface-whitelist.txt +0 -27
- package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_assert_story_files.sh +0 -261
- package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_file_surface.sh +0 -210
- package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_flashcard_gate.sh +0 -190
- package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_prep_qa_context.sh +0 -482
- package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_test_ratchet.sh +0 -327
- package/dist/templates/cleargate-planning/.cleargate/scripts/test_ratchet.mjs +0 -261
- package/dist/templates/cleargate-planning/.cleargate/scripts/update_state.mjs +0 -246
- package/dist/templates/cleargate-planning/.cleargate/scripts/validate_bounce_readiness.mjs +0 -111
- package/dist/templates/cleargate-planning/.cleargate/scripts/validate_state.mjs +0 -184
- package/dist/templates/cleargate-planning/.cleargate/scripts/write_dispatch.sh +0 -172
- package/dist/templates/cleargate-planning/.cleargate/templates/Bug.md +0 -126
- package/dist/templates/cleargate-planning/.cleargate/templates/CR.md +0 -130
- package/dist/templates/cleargate-planning/.cleargate/templates/Sprint Plan Template.md +0 -137
- package/dist/templates/cleargate-planning/.cleargate/templates/epic.md +0 -166
- package/dist/templates/cleargate-planning/.cleargate/templates/hotfix.md +0 -111
- package/dist/templates/cleargate-planning/.cleargate/templates/initiative.md +0 -122
- package/dist/templates/cleargate-planning/.cleargate/templates/sprint_context.md +0 -50
- package/dist/templates/cleargate-planning/.cleargate/templates/sprint_report.md +0 -224
- package/dist/templates/cleargate-planning/.cleargate/templates/story.md +0 -213
- package/dist/templates/cleargate-planning/CLAUDE.md +0 -66
- package/dist/templates/cleargate-planning/MANIFEST.json +0 -503
- package/dist/templates/synthesis/active-sprint.md +0 -30
- package/dist/templates/synthesis/open-gates.md +0 -38
- package/dist/templates/synthesis/product-state.md +0 -31
- package/dist/templates/synthesis/roadmap.md +0 -63
- package/dist/whoami-EANGN46Z.js.map +0 -1
|
@@ -1,341 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* launch_wave.mjs — STORY-033-04 (EPIC-033 capstone): Parallel-Wave Execution + Barrier.
|
|
4
|
-
*
|
|
5
|
-
* Drives a `parallel()` Workflow of one worktree-isolated segment per story in an
|
|
6
|
-
* Architect-planned wave (waves.json, produced by STORY-033-03), mints a stable per-thunk
|
|
7
|
-
* RUN_ID for each segment, and returns the array of schema-typed segment verdicts to the
|
|
8
|
-
* Orchestrator at the barrier. The Orchestrator consolidates serially: validate → process
|
|
9
|
-
* flashcards (between-wave) → merge GREEN stories one worktree at a time → restore env.
|
|
10
|
-
*
|
|
11
|
-
* SPRINT-32 constraint (SDR §2.1): this sprint BUILDS the wave capability but runs SERIALLY.
|
|
12
|
-
* No live `parallel()` dispatch executes here. The launcher ships for the NEXT (self-hosting)
|
|
13
|
-
* sprint. The testable JS surface is the exported `validateVerdicts` barrier validator (plus
|
|
14
|
-
* the helper constructors); the `parallel()` dispatch itself is exercised only when the
|
|
15
|
-
* next sprint runs under `execution_mode: v2-parallel`.
|
|
16
|
-
*
|
|
17
|
-
* Spike facts honored (STORY-033-01-spike-result.md, FLASHCARD #workflow #token-ledger #worktree):
|
|
18
|
-
* - Per-agent SubagentStop attribution is DEAD under workflows → the barrier writes one
|
|
19
|
-
* ledger row per segment from `verdict.tokens`, keyed by RUN_ID (STORY-033-02 owns the write).
|
|
20
|
-
* - `PreToolUse:Task` never fires under workflows → no dispatch-marker auto-attribution.
|
|
21
|
-
* - Per-thunk child env is NOT settable → the Orchestrator sets `SKIP_FLASHCARD_GATE=1` in
|
|
22
|
-
* its OWN env before launch (inherited by all children) and restores it at the barrier.
|
|
23
|
-
* - `isolation:'worktree'` checks out tracked-files-only off the wrong base → segments use
|
|
24
|
-
* ClearGate's OWN `git worktree add .worktrees/STORY-X -b story/STORY-X sprint/S-NN`.
|
|
25
|
-
* - `resumeFromRunId` caches completed GREEN agents (0 tokens on replay) → complete-then-resume
|
|
26
|
-
* re-dispatches only the ESCALATED/BLOCKED segment.
|
|
27
|
-
*
|
|
28
|
-
* Kill-switch: `execution_mode: v2-serial` in sprint frontmatter OR `CLEARGATE_PARALLEL_WAVES=off`
|
|
29
|
-
* in the session env routes to today's serial Phase C loop with ZERO behavior change — no
|
|
30
|
-
* launch_wave.mjs invocation occurs on that path.
|
|
31
|
-
*
|
|
32
|
-
* Verdict schema (discriminated union, STORY-033-04 §1.2 — inherits protocol §23):
|
|
33
|
-
* {
|
|
34
|
-
* verdict: 'GREEN' | 'ESCALATED' | 'BLOCKED',
|
|
35
|
-
* storyId: string,
|
|
36
|
-
* runId: string,
|
|
37
|
-
* devSha: string,
|
|
38
|
-
* qaSha: string,
|
|
39
|
-
* archSha?: string,
|
|
40
|
-
* flashcards_flagged: string[],
|
|
41
|
-
* counters: { qa_bounces: number, arch_bounces: number, breaker_hits: number },
|
|
42
|
-
* tokens: { input, output, cache_creation, cache_read, model },
|
|
43
|
-
* blocker?: { type, message } // REQUIRED iff verdict !== 'GREEN'
|
|
44
|
-
* }
|
|
45
|
-
* blocker.type ∈ the five §22 true-blocker kinds.
|
|
46
|
-
*/
|
|
47
|
-
|
|
48
|
-
import crypto from 'node:crypto';
|
|
49
|
-
|
|
50
|
-
// ---------------------------------------------------------------------------
|
|
51
|
-
// Constants — the verdict discriminated-union vocabulary (protocol §23).
|
|
52
|
-
// ---------------------------------------------------------------------------
|
|
53
|
-
|
|
54
|
-
/** The three terminal verdict states a segment may return at the barrier. */
|
|
55
|
-
export const VERDICT_KINDS = Object.freeze(['GREEN', 'ESCALATED', 'BLOCKED']);
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* The five protocol-§22 true-blocker kinds. A non-GREEN verdict's `blocker.type`
|
|
59
|
-
* MUST be one of these (inherited verbatim from §22 — do not redefine here).
|
|
60
|
-
*/
|
|
61
|
-
export const BLOCKER_TYPES = Object.freeze([
|
|
62
|
-
'destructive', // force-push, git reset --hard, drop table, delete untracked file, kill process
|
|
63
|
-
'secret', // reading .env / fetching a secret / persisting credential material
|
|
64
|
-
'user-intent', // a genuine "what does the user want?" not inferable from goal/Gherkin
|
|
65
|
-
'technical-impossibility', // required infra unavailable (test DB unreachable, MCP down)
|
|
66
|
-
'spec-contradiction', // story Gherkin self-contradicts or two wave stories collide on a shared surface
|
|
67
|
-
]);
|
|
68
|
-
|
|
69
|
-
/** Required scalar fields present on EVERY verdict regardless of discriminant. */
|
|
70
|
-
const REQUIRED_SCALAR_FIELDS = Object.freeze(['storyId', 'runId', 'devSha', 'qaSha']);
|
|
71
|
-
|
|
72
|
-
/** Required `tokens` sub-fields (per-segment cost the barrier writes to the ledger). */
|
|
73
|
-
const REQUIRED_TOKEN_FIELDS = Object.freeze([
|
|
74
|
-
'input',
|
|
75
|
-
'output',
|
|
76
|
-
'cache_creation',
|
|
77
|
-
'cache_read',
|
|
78
|
-
'model',
|
|
79
|
-
]);
|
|
80
|
-
|
|
81
|
-
/** Required `counters` sub-fields. */
|
|
82
|
-
const REQUIRED_COUNTER_FIELDS = Object.freeze(['qa_bounces', 'arch_bounces', 'breaker_hits']);
|
|
83
|
-
|
|
84
|
-
// ---------------------------------------------------------------------------
|
|
85
|
-
// RUN_ID minting — one stable, distinct id per wave segment.
|
|
86
|
-
// ---------------------------------------------------------------------------
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Mint a stable, distinct RUN_ID for a wave segment. Stable so a resumeFromRunId replay
|
|
90
|
-
* can re-key the same segment; distinct so sibling segments do not collide on the
|
|
91
|
-
* RUN_ID-keyed ledger row STORY-033-02 writes.
|
|
92
|
-
*
|
|
93
|
-
* @param {string} storyId The story this segment executes (e.g. "STORY-033-04").
|
|
94
|
-
* @param {string} sprintId The active sprint (e.g. "SPRINT-32").
|
|
95
|
-
* @returns {string} A run id of the form "run-<sprint>-<story>-<8hex>".
|
|
96
|
-
*/
|
|
97
|
-
export function mintRunId(storyId, sprintId) {
|
|
98
|
-
const entropy = crypto.randomBytes(4).toString('hex');
|
|
99
|
-
const story = String(storyId || 'STORY-UNKNOWN').replace(/[^A-Za-z0-9-]/g, '');
|
|
100
|
-
const sprint = String(sprintId || 'SPRINT-UNKNOWN').replace(/[^A-Za-z0-9-]/g, '');
|
|
101
|
-
return `run-${sprint}-${story}-${entropy}`;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// ---------------------------------------------------------------------------
|
|
105
|
-
// Worktree command construction (spike decision 2 — ClearGate-managed worktrees).
|
|
106
|
-
// ---------------------------------------------------------------------------
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Build the bash `git worktree add` command for a segment. ClearGate manages its OWN
|
|
110
|
-
* worktrees (NOT the Workflow tool's `isolation:'worktree'`, which checks out
|
|
111
|
-
* tracked-files-only off the wrong base — spike decision 2). Worktrees are pre-created
|
|
112
|
-
* SERIALLY at wave launch to avoid concurrent `git worktree add` racing the repo index.
|
|
113
|
-
*
|
|
114
|
-
* @param {string} storyId e.g. "STORY-033-04"
|
|
115
|
-
* @param {string} sprintBranch e.g. "sprint/S-32"
|
|
116
|
-
* @returns {string} the bash command string
|
|
117
|
-
*/
|
|
118
|
-
export function worktreeAddCommand(storyId, sprintBranch) {
|
|
119
|
-
const story = String(storyId);
|
|
120
|
-
return `git worktree add .worktrees/${story} -b story/${story} ${sprintBranch}`;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// ---------------------------------------------------------------------------
|
|
124
|
-
// Kill-switch — does this run go parallel, or revert to the serial Phase C loop?
|
|
125
|
-
// ---------------------------------------------------------------------------
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Decide whether the wave loop should run in parallel. Returns false (serial) when EITHER
|
|
129
|
-
* the sprint frontmatter `execution_mode` is `v2-serial` OR the session env carries
|
|
130
|
-
* `CLEARGATE_PARALLEL_WAVES=off`. Either revert path runs today's serial five-dispatch
|
|
131
|
-
* Phase C loop with ZERO behavior change — no launch_wave.mjs invocation occurs.
|
|
132
|
-
*
|
|
133
|
-
* @param {string|undefined} executionMode sprint-frontmatter `execution_mode` value
|
|
134
|
-
* @param {NodeJS.ProcessEnv} [env] session env (defaults to process.env)
|
|
135
|
-
* @returns {boolean} true → parallel waves; false → serial loop
|
|
136
|
-
*/
|
|
137
|
-
export function shouldRunParallel(executionMode, env = process.env) {
|
|
138
|
-
if ((env.CLEARGATE_PARALLEL_WAVES || '').toLowerCase() === 'off') return false;
|
|
139
|
-
if (executionMode === 'v2-serial') return false;
|
|
140
|
-
return executionMode === 'v2-parallel';
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// ---------------------------------------------------------------------------
|
|
144
|
-
// Barrier verdict validator (EXPORTED — STORY-033-04 §1.2 / §4.1).
|
|
145
|
-
// ---------------------------------------------------------------------------
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Internal: validate a single verdict, returning a human-readable reason string when it
|
|
149
|
-
* is malformed, or null when it is well-formed. Never throws — the array-level
|
|
150
|
-
* `validateVerdicts` owns the throw so it can name the offending storyId.
|
|
151
|
-
*
|
|
152
|
-
* @param {unknown} v
|
|
153
|
-
* @returns {string|null} malformation reason, or null if valid
|
|
154
|
-
*/
|
|
155
|
-
function verdictMalformationReason(v) {
|
|
156
|
-
if (v === null || typeof v !== 'object') {
|
|
157
|
-
return 'verdict is not an object';
|
|
158
|
-
}
|
|
159
|
-
const verdict = /** @type {Record<string, unknown>} */ (v);
|
|
160
|
-
|
|
161
|
-
// Discriminant must be one of the three known verdict kinds.
|
|
162
|
-
if (!VERDICT_KINDS.includes(/** @type {string} */ (verdict.verdict))) {
|
|
163
|
-
return `'verdict' must be one of ${VERDICT_KINDS.join(', ')} (got ${JSON.stringify(verdict.verdict)})`;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// Required scalar fields.
|
|
167
|
-
for (const field of REQUIRED_SCALAR_FIELDS) {
|
|
168
|
-
if (typeof verdict[field] !== 'string' || verdict[field] === '') {
|
|
169
|
-
return `missing or empty required field '${field}'`;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// flashcards_flagged must be an array (may be empty).
|
|
174
|
-
if (!Array.isArray(verdict.flashcards_flagged)) {
|
|
175
|
-
return `'flashcards_flagged' must be an array`;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// counters object with the three required numeric sub-fields.
|
|
179
|
-
const counters = verdict.counters;
|
|
180
|
-
if (counters === null || typeof counters !== 'object') {
|
|
181
|
-
return `missing required 'counters' object`;
|
|
182
|
-
}
|
|
183
|
-
for (const field of REQUIRED_COUNTER_FIELDS) {
|
|
184
|
-
if (typeof (/** @type {Record<string, unknown>} */ (counters))[field] !== 'number') {
|
|
185
|
-
return `'counters.${field}' must be a number`;
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// tokens object with the five required sub-fields (per-segment cost the ledger consumes).
|
|
190
|
-
const tokens = verdict.tokens;
|
|
191
|
-
if (tokens === null || typeof tokens !== 'object') {
|
|
192
|
-
return `missing required 'tokens' object`;
|
|
193
|
-
}
|
|
194
|
-
for (const field of REQUIRED_TOKEN_FIELDS) {
|
|
195
|
-
const val = (/** @type {Record<string, unknown>} */ (tokens))[field];
|
|
196
|
-
if (field === 'model') {
|
|
197
|
-
if (typeof val !== 'string' || val === '') return `'tokens.model' must be a non-empty string`;
|
|
198
|
-
} else if (typeof val !== 'number') {
|
|
199
|
-
return `'tokens.${field}' must be a number`;
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// blocker is REQUIRED iff verdict !== GREEN (§1.2).
|
|
204
|
-
if (verdict.verdict !== 'GREEN') {
|
|
205
|
-
const blocker = verdict.blocker;
|
|
206
|
-
if (blocker === null || blocker === undefined || typeof blocker !== 'object') {
|
|
207
|
-
return `non-GREEN verdict (${verdict.verdict}) is missing the required 'blocker' object`;
|
|
208
|
-
}
|
|
209
|
-
const b = /** @type {Record<string, unknown>} */ (blocker);
|
|
210
|
-
if (!BLOCKER_TYPES.includes(/** @type {string} */ (b.type))) {
|
|
211
|
-
return `'blocker.type' must be one of the §22 true-blocker kinds ${BLOCKER_TYPES.join(', ')} (got ${JSON.stringify(b.type)})`;
|
|
212
|
-
}
|
|
213
|
-
if (typeof b.message !== 'string' || b.message === '') {
|
|
214
|
-
return `'blocker.message' must be a non-empty string`;
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
return null;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* Barrier validator. Accepts a well-formed verdict array; raises a validation Error NAMING
|
|
223
|
-
* the offending `storyId` on the first malformed verdict (missing required field, unknown
|
|
224
|
-
* discriminant, or non-GREEN without a valid `blocker`). The named segment is the one the
|
|
225
|
-
* Orchestrator marks ESCALATED (no ledger row); sibling GREEN verdicts consolidate.
|
|
226
|
-
*
|
|
227
|
-
* @param {unknown[]} verdicts
|
|
228
|
-
* @returns {void}
|
|
229
|
-
* @throws {Error} when any verdict is malformed — message contains the offending storyId.
|
|
230
|
-
*/
|
|
231
|
-
export function validateVerdicts(verdicts) {
|
|
232
|
-
if (!Array.isArray(verdicts)) {
|
|
233
|
-
throw new Error(
|
|
234
|
-
`validateVerdicts: expected an array of segment verdicts, got ${typeof verdicts}.`,
|
|
235
|
-
);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
for (const v of verdicts) {
|
|
239
|
-
const reason = verdictMalformationReason(v);
|
|
240
|
-
if (reason !== null) {
|
|
241
|
-
// Name the offending storyId so the Orchestrator can mark exactly that segment
|
|
242
|
-
// ESCALATED. Fall back to a placeholder when the verdict has no usable storyId.
|
|
243
|
-
const offendingId =
|
|
244
|
-
v !== null && typeof v === 'object' && typeof v.storyId === 'string' && v.storyId !== ''
|
|
245
|
-
? v.storyId
|
|
246
|
-
: '<unknown-storyId>';
|
|
247
|
-
throw new Error(`Malformed segment verdict for ${offendingId}: ${reason}.`);
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// ---------------------------------------------------------------------------
|
|
253
|
-
// launchWave — the parallel() driver (next-sprint surface; not exercised this sprint).
|
|
254
|
-
// ---------------------------------------------------------------------------
|
|
255
|
-
|
|
256
|
-
/**
|
|
257
|
-
* Drive a `parallel()` Workflow of one segment per story in the wave. Each segment mints a
|
|
258
|
-
* stable RUN_ID, runs the linear per-story pipeline inside its own ClearGate-managed
|
|
259
|
-
* `.worktrees/STORY-X`, accumulates `tokens` from the outset, and RETURNS a verdict object —
|
|
260
|
-
* it never blocks (a §22 true-blocker becomes a BLOCKED verdict with `blocker.type`, never an
|
|
261
|
-
* `AskUserQuestion`).
|
|
262
|
-
*
|
|
263
|
-
* Flashcard write-gate (BUG-034 — code-enforced restore). Per the spike, per-thunk child env
|
|
264
|
-
* is not settable, so the WRITE-gate is suppressed by setting `SKIP_FLASHCARD_GATE=1` in the
|
|
265
|
-
* env inherited by every segment for the duration of the `parallel()` dispatch. This launcher
|
|
266
|
-
* OWNS that save/set/restore in a `try/finally`: the prior value is snapshotted, `=1` is set
|
|
267
|
-
* before dispatch, and the EXACT prior value is restored (deleted iff it was unset) in a
|
|
268
|
-
* `finally` that runs even when `parallel()` or `validateVerdicts()` throws. Previously this
|
|
269
|
-
* was an Orchestrator prose obligation (SKILL.md §C.0.1); a mid-barrier throw left the gate
|
|
270
|
-
* suppressed session-wide, leaking into the serial fallback EPIC-033 promises is untouched.
|
|
271
|
-
*
|
|
272
|
-
* `validateVerdicts()` (the designed throw site) runs INSIDE the protected region so the
|
|
273
|
-
* `finally` always restores the gate before the validation Error propagates to the caller.
|
|
274
|
-
*
|
|
275
|
-
* The `parallel`/`segmentRunner` seams are injected so this is testable without a live
|
|
276
|
-
* Workflow runtime (SPRINT-32 runs serial — no real dispatch). When omitted, the function
|
|
277
|
-
* documents the shape and performs NO env mutation; the live launcher wires the Workflow
|
|
278
|
-
* `parallel()` primitive.
|
|
279
|
-
*
|
|
280
|
-
* @param {object} args
|
|
281
|
-
* @param {string} args.sprintId
|
|
282
|
-
* @param {string} args.sprintBranch
|
|
283
|
-
* @param {{ wave: string, stories: string[], parallel: boolean }} args.wave one wave from waves.json
|
|
284
|
-
* @param {(thunks: Array<() => Promise<object>>) => Promise<object[]>} [args.parallel] parallel() seam
|
|
285
|
-
* @param {(story: string, runId: string) => Promise<object>} [args.segmentRunner] per-segment seam
|
|
286
|
-
* @param {NodeJS.ProcessEnv} [args.env] env handle for the gate save/restore (defaults to process.env)
|
|
287
|
-
* @returns {Promise<object[]>} the validated array of segment verdicts
|
|
288
|
-
* @throws {Error} from validateVerdicts when any verdict is malformed — gate is restored first.
|
|
289
|
-
*/
|
|
290
|
-
export async function launchWave({ sprintId, sprintBranch, wave, parallel, segmentRunner, env = process.env }) {
|
|
291
|
-
const stories = (wave && Array.isArray(wave.stories) ? wave.stories : []).slice();
|
|
292
|
-
|
|
293
|
-
// Pre-create each wave story's worktree SERIALLY (spike risk-mitigation: concurrent
|
|
294
|
-
// `git worktree add` races the repo index). The actual exec is the Orchestrator's job;
|
|
295
|
-
// here we surface the commands so the launcher / tests can assert their construction.
|
|
296
|
-
const worktreeCommands = stories.map((story) => worktreeAddCommand(story, sprintBranch));
|
|
297
|
-
|
|
298
|
-
// Build one thunk per story; each mints its own distinct RUN_ID.
|
|
299
|
-
const segmentPlan = stories.map((story) => ({ story, runId: mintRunId(story, sprintId) }));
|
|
300
|
-
|
|
301
|
-
if (typeof parallel !== 'function' || typeof segmentRunner !== 'function') {
|
|
302
|
-
// No live runtime seam supplied (the SPRINT-32 serial-build case). Return the plan so a
|
|
303
|
-
// caller can inspect the worktree commands + minted RUN_IDs without dispatching agents.
|
|
304
|
-
// NO env mutation here — nothing is dispatched, so the gate need not be suppressed.
|
|
305
|
-
return segmentPlan.map((seg) => ({
|
|
306
|
-
storyId: seg.story,
|
|
307
|
-
runId: seg.runId,
|
|
308
|
-
worktreeCommand: worktreeAddCommand(seg.story, sprintBranch),
|
|
309
|
-
planned: true,
|
|
310
|
-
}));
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
// BUG-034: snapshot the PRIOR gate value, suppress it for the dispatch, restore it EXACTLY
|
|
314
|
-
// in a `finally` — even if parallel()/validateVerdicts() throws. `hasOwnProperty` (not a
|
|
315
|
-
// truthiness check) distinguishes "unset" from "set to empty string" so restore is exact.
|
|
316
|
-
const hadPriorGate = Object.prototype.hasOwnProperty.call(env, 'SKIP_FLASHCARD_GATE');
|
|
317
|
-
const priorGate = env.SKIP_FLASHCARD_GATE;
|
|
318
|
-
env.SKIP_FLASHCARD_GATE = '1';
|
|
319
|
-
try {
|
|
320
|
-
const thunks = segmentPlan.map((seg) => () => segmentRunner(seg.story, seg.runId));
|
|
321
|
-
const verdicts = await parallel(thunks);
|
|
322
|
-
// Designed throw site (names the offending storyId). Inside the try so the finally below
|
|
323
|
-
// restores the gate before the Error propagates to the Orchestrator.
|
|
324
|
-
validateVerdicts(verdicts);
|
|
325
|
-
void worktreeCommands; // pre-created by the Orchestrator before the parallel() call
|
|
326
|
-
return verdicts;
|
|
327
|
-
} finally {
|
|
328
|
-
if (hadPriorGate) env.SKIP_FLASHCARD_GATE = priorGate;
|
|
329
|
-
else delete env.SKIP_FLASHCARD_GATE;
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// When run directly (`node launch_wave.mjs`) print a short usage note — there is no live
|
|
334
|
-
// wave to drive in this sprint, so direct execution is informational only.
|
|
335
|
-
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
336
|
-
process.stdout.write(
|
|
337
|
-
'launch_wave.mjs — parallel-wave launcher (EPIC-033). ' +
|
|
338
|
-
'Exports validateVerdicts, mintRunId, worktreeAddCommand, shouldRunParallel, launchWave. ' +
|
|
339
|
-
'Not invoked under execution_mode: v2-serial or CLEARGATE_PARALLEL_WAVES=off (serial Phase C loop).\n',
|
|
340
|
-
);
|
|
341
|
-
}
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* report-filename.mjs — Shared helper for computing the sprint report filename.
|
|
4
|
-
*
|
|
5
|
-
* Named export only — no default.
|
|
6
|
-
*
|
|
7
|
-
* Dependencies: node:path, node:fs only. No third-party deps.
|
|
8
|
-
*
|
|
9
|
-
* Design notes:
|
|
10
|
-
* - Helper is pure given its arguments + a filesystem read (when opts.forRead=true).
|
|
11
|
-
* - Never throws — callers decide whether to fs.readFileSync and handle ENOENT.
|
|
12
|
-
* - Never reads env vars. Sprint dir resolution stays in callers.
|
|
13
|
-
* (Each consumer owns CLEARGATE_SPRINT_DIR resolution.)
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import path from 'node:path';
|
|
17
|
-
import fs from 'node:fs';
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Compute the report filename for a given sprint directory + sprint ID.
|
|
21
|
-
*
|
|
22
|
-
* New naming (SPRINT-18+): SPRINT-<#>_REPORT.md where <#> is the numeric
|
|
23
|
-
* portion of the sprint-id (e.g. "18" for "SPRINT-18").
|
|
24
|
-
* No-numeric-portion ids (e.g. "SPRINT-TEST") → plain REPORT.md.
|
|
25
|
-
*
|
|
26
|
-
* Backwards-compat read-fallback: when opts.forRead === true AND the new-name
|
|
27
|
-
* file is absent BUT legacy REPORT.md exists, return the legacy path.
|
|
28
|
-
* Covers SPRINT-01..17 archives written before STORY-025-03's naming change.
|
|
29
|
-
* MUST NOT rename or rewrite those pre-existing files.
|
|
30
|
-
*
|
|
31
|
-
* @param {string} sprintDirPath absolute path to the sprint directory
|
|
32
|
-
* @param {string} sprintId e.g. "SPRINT-18" or "SPRINT-TEST"
|
|
33
|
-
* @param {{ forRead?: boolean }} [opts]
|
|
34
|
-
* @returns {string} absolute path to the report file
|
|
35
|
-
*/
|
|
36
|
-
export function reportFilename(sprintDirPath, sprintId, opts) {
|
|
37
|
-
const numMatch = sprintId.match(/^SPRINT-(\d+)$/);
|
|
38
|
-
if (!numMatch) {
|
|
39
|
-
// No numeric portion — use plain REPORT.md (e.g. SPRINT-TEST)
|
|
40
|
-
return path.join(sprintDirPath, 'REPORT.md');
|
|
41
|
-
}
|
|
42
|
-
const sprintNumber = numMatch[1];
|
|
43
|
-
const newName = path.join(sprintDirPath, `SPRINT-${sprintNumber}_REPORT.md`);
|
|
44
|
-
|
|
45
|
-
// Read-fallback: if the new-name file doesn't exist but legacy REPORT.md does, use legacy.
|
|
46
|
-
if (opts?.forRead) {
|
|
47
|
-
const legacyName = path.join(sprintDirPath, 'REPORT.md');
|
|
48
|
-
if (!fs.existsSync(newName) && fs.existsSync(legacyName)) {
|
|
49
|
-
return legacyName;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
return newName;
|
|
54
|
-
}
|
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# pre_gate_common.sh — Shared helpers for pre_gate_runner.sh
|
|
3
|
-
# Sourced by pre_gate_runner.sh. Do NOT execute directly.
|
|
4
|
-
# Handles Node+TS stacks only (no Python/Rust/Go/Java/Swift detectors).
|
|
5
|
-
set -euo pipefail
|
|
6
|
-
|
|
7
|
-
# ---------------------------------------------------------------------------
|
|
8
|
-
# read_config_field <field_path> <config_file>
|
|
9
|
-
# Uses node -p to parse JSON and extract a field.
|
|
10
|
-
# field_path: dot-separated path, e.g. "qa.typecheck"
|
|
11
|
-
# ---------------------------------------------------------------------------
|
|
12
|
-
read_config_field() {
|
|
13
|
-
local field_path="$1"
|
|
14
|
-
local config_file="$2"
|
|
15
|
-
node -p "
|
|
16
|
-
const c = JSON.parse(require('fs').readFileSync('${config_file}', 'utf8'));
|
|
17
|
-
const parts = '${field_path}'.split('.');
|
|
18
|
-
let v = c;
|
|
19
|
-
for (const p of parts) { v = v[p]; }
|
|
20
|
-
Array.isArray(v) ? JSON.stringify(v) : (v === undefined ? '' : String(v));
|
|
21
|
-
" 2>/dev/null || echo ""
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
# ---------------------------------------------------------------------------
|
|
25
|
-
# get_modified_files <worktree_path>
|
|
26
|
-
# Lists files modified in the current git index vs HEAD.
|
|
27
|
-
# ---------------------------------------------------------------------------
|
|
28
|
-
get_modified_files() {
|
|
29
|
-
local worktree="$1"
|
|
30
|
-
git -C "$worktree" diff --name-only HEAD 2>/dev/null || true
|
|
31
|
-
git -C "$worktree" diff --cached --name-only 2>/dev/null || true
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
# ---------------------------------------------------------------------------
|
|
35
|
-
# get_staged_diff <worktree_path>
|
|
36
|
-
# Returns unified diff of staged changes.
|
|
37
|
-
# ---------------------------------------------------------------------------
|
|
38
|
-
get_staged_diff() {
|
|
39
|
-
local worktree="$1"
|
|
40
|
-
git -C "$worktree" diff --cached 2>/dev/null || git -C "$worktree" diff HEAD 2>/dev/null || true
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
# ---------------------------------------------------------------------------
|
|
44
|
-
# record_result <report_file> <check_name> <status> [details]
|
|
45
|
-
# Appends one result line to the report.
|
|
46
|
-
# status: PASS | FAIL | WARN | INFO
|
|
47
|
-
# ---------------------------------------------------------------------------
|
|
48
|
-
record_result() {
|
|
49
|
-
local report_file="$1"
|
|
50
|
-
local check_name="$2"
|
|
51
|
-
local status="$3"
|
|
52
|
-
local details="${4:-}"
|
|
53
|
-
echo "[${status}] ${check_name}: ${details}" >> "$report_file"
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
# ---------------------------------------------------------------------------
|
|
57
|
-
# print_summary <report_file>
|
|
58
|
-
# Prints count of PASS/FAIL/WARN lines from the report.
|
|
59
|
-
# ---------------------------------------------------------------------------
|
|
60
|
-
print_summary() {
|
|
61
|
-
local report_file="$1"
|
|
62
|
-
local pass_count fail_count warn_count
|
|
63
|
-
pass_count=$(grep -c '^\[PASS\]' "$report_file" 2>/dev/null || echo 0)
|
|
64
|
-
fail_count=$(grep -c '^\[FAIL\]' "$report_file" 2>/dev/null || echo 0)
|
|
65
|
-
warn_count=$(grep -c '^\[WARN\]' "$report_file" 2>/dev/null || echo 0)
|
|
66
|
-
echo "Summary: ${pass_count} passed, ${fail_count} failed, ${warn_count} warnings"
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
# ---------------------------------------------------------------------------
|
|
70
|
-
# write_report <report_file> <mode> <worktree_path> <branch>
|
|
71
|
-
# Writes the report header.
|
|
72
|
-
# ---------------------------------------------------------------------------
|
|
73
|
-
write_report_header() {
|
|
74
|
-
local report_file="$1"
|
|
75
|
-
local mode="$2"
|
|
76
|
-
local worktree="$3"
|
|
77
|
-
local branch="$4"
|
|
78
|
-
mkdir -p "$(dirname "$report_file")"
|
|
79
|
-
{
|
|
80
|
-
echo "# ClearGate Pre-Gate Scan Report"
|
|
81
|
-
echo "Mode: ${mode}"
|
|
82
|
-
echo "Worktree: ${worktree}"
|
|
83
|
-
echo "Branch: ${branch}"
|
|
84
|
-
echo "Timestamp: $(date -u '+%Y-%m-%dT%H:%M:%SZ')"
|
|
85
|
-
echo "---"
|
|
86
|
-
} > "$report_file"
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
# ---------------------------------------------------------------------------
|
|
90
|
-
# detect_stack <worktree_path>
|
|
91
|
-
# Returns "node-ts" if package.json + tsconfig.json found, else "unknown".
|
|
92
|
-
# Node+TS only — no Python/Rust/Go/Java/Swift detectors.
|
|
93
|
-
# ---------------------------------------------------------------------------
|
|
94
|
-
detect_stack() {
|
|
95
|
-
local worktree="$1"
|
|
96
|
-
if [[ -f "${worktree}/package.json" ]]; then
|
|
97
|
-
if [[ -f "${worktree}/tsconfig.json" ]]; then
|
|
98
|
-
echo "node-ts"
|
|
99
|
-
else
|
|
100
|
-
echo "node"
|
|
101
|
-
fi
|
|
102
|
-
else
|
|
103
|
-
echo "unknown"
|
|
104
|
-
fi
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
# ---------------------------------------------------------------------------
|
|
108
|
-
# resolve_story_id_from_branch <branch>
|
|
109
|
-
# Extracts story/CR/bug ID from a branch name like story/STORY-022-04.
|
|
110
|
-
# Pattern (per M4 plan §1): (story|cr|bug)/([A-Z-]+-[0-9]+(-[0-9]+)?)
|
|
111
|
-
# Returns the ID (group 2) on stdout, or empty string on no match.
|
|
112
|
-
# Cross-OS: bash 3.2+, POSIX ERE grep -E, quoted expansions, no sed -E extensions.
|
|
113
|
-
# ---------------------------------------------------------------------------
|
|
114
|
-
resolve_story_id_from_branch() {
|
|
115
|
-
local branch="$1"
|
|
116
|
-
# Match the full pattern and extract just the ID part after the slash
|
|
117
|
-
local matched
|
|
118
|
-
matched="$(printf '%s\n' "${branch}" | grep -oE '(story|cr|bug)/[A-Z][A-Z-]*[A-Z]-[0-9]+(-[0-9]+)?' | head -1 || true)"
|
|
119
|
-
if [[ -n "${matched}" ]]; then
|
|
120
|
-
# Strip the (story|cr|bug)/ prefix — everything up to and including first /
|
|
121
|
-
printf '%s\n' "${matched#*/}"
|
|
122
|
-
else
|
|
123
|
-
printf ''
|
|
124
|
-
fi
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
# ---------------------------------------------------------------------------
|
|
128
|
-
# resolve_lane <state_json_path> <story_id>
|
|
129
|
-
# Reads the lane field for a story from state.json.
|
|
130
|
-
# Returns "standard" if story absent, lane absent, or jq fails.
|
|
131
|
-
# jq 1.5+ syntax. Cross-OS portable.
|
|
132
|
-
# ---------------------------------------------------------------------------
|
|
133
|
-
resolve_lane() {
|
|
134
|
-
local state_json="$1"
|
|
135
|
-
local story_id="$2"
|
|
136
|
-
if [[ ! -f "${state_json}" ]]; then
|
|
137
|
-
printf 'standard\n'
|
|
138
|
-
return
|
|
139
|
-
fi
|
|
140
|
-
local lane
|
|
141
|
-
lane="$(jq -r --arg sid "${story_id}" '.stories[$sid].lane // "standard"' "${state_json}" 2>/dev/null || printf 'standard')"
|
|
142
|
-
if [[ -z "${lane}" || "${lane}" = "null" ]]; then
|
|
143
|
-
lane="standard"
|
|
144
|
-
fi
|
|
145
|
-
printf '%s\n' "${lane}"
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
# ---------------------------------------------------------------------------
|
|
149
|
-
# append_ld_event <sprint_md_path> <story_id> <reason>
|
|
150
|
-
# Appends an LD event row to the sprint markdown §4 Events section.
|
|
151
|
-
# If "## 4. Events" heading absent, creates it with table header first.
|
|
152
|
-
# Idempotent by design (orchestrator calls once per demotion).
|
|
153
|
-
# Cross-OS: bash 3.2+, portable date, printf.
|
|
154
|
-
# ---------------------------------------------------------------------------
|
|
155
|
-
append_ld_event() {
|
|
156
|
-
local sprint_md="$1"
|
|
157
|
-
local story_id="$2"
|
|
158
|
-
local reason="$3"
|
|
159
|
-
local timestamp
|
|
160
|
-
timestamp="$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
|
|
161
|
-
# Truncate reason to 80 chars
|
|
162
|
-
reason="$(printf '%s' "${reason}" | cut -c1-80)"
|
|
163
|
-
|
|
164
|
-
if [[ ! -f "${sprint_md}" ]]; then
|
|
165
|
-
printf 'append_ld_event: sprint markdown not found: %s\n' "${sprint_md}" >&2
|
|
166
|
-
return 1
|
|
167
|
-
fi
|
|
168
|
-
|
|
169
|
-
local row
|
|
170
|
-
row="$(printf '| LD | %s | %s | %s |' "${story_id}" "${timestamp}" "${reason}")"
|
|
171
|
-
|
|
172
|
-
if grep -q '^## 4\. Events' "${sprint_md}" 2>/dev/null; then
|
|
173
|
-
# Section exists: append row only
|
|
174
|
-
printf '\n%s\n' "${row}" >> "${sprint_md}"
|
|
175
|
-
else
|
|
176
|
-
# Section absent: append heading + table header + row
|
|
177
|
-
printf '\n## 4. Events\n\n| Event | Story | Timestamp | Reason |\n|---|---|---|---|\n%s\n' "${row}" >> "${sprint_md}"
|
|
178
|
-
fi
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
# ---------------------------------------------------------------------------
|
|
182
|
-
# diff_package_json <worktree_path> <branch>
|
|
183
|
-
# Prints new runtime deps (non-dev) introduced vs <branch>^.
|
|
184
|
-
# Returns lines like: "new runtime dep: <name>"
|
|
185
|
-
# ---------------------------------------------------------------------------
|
|
186
|
-
diff_package_json() {
|
|
187
|
-
local worktree="$1"
|
|
188
|
-
local branch="$2"
|
|
189
|
-
|
|
190
|
-
# Get old package.json from branch parent (branch^ in the worktree's git)
|
|
191
|
-
local old_json
|
|
192
|
-
old_json=$(git -C "$worktree" show "${branch}^:package.json" 2>/dev/null || echo '{"dependencies":{}}')
|
|
193
|
-
|
|
194
|
-
local new_json
|
|
195
|
-
new_json=$(cat "${worktree}/package.json" 2>/dev/null || echo '{"dependencies":{}}')
|
|
196
|
-
|
|
197
|
-
# Extract dep keys using node
|
|
198
|
-
node -e "
|
|
199
|
-
const oldPkg = JSON.parse($(echo "$old_json" | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>process.stdout.write(JSON.stringify(d)))"));
|
|
200
|
-
const newPkg = JSON.parse($(echo "$new_json" | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>process.stdout.write(JSON.stringify(d)))"));
|
|
201
|
-
const oldDeps = Object.keys(oldPkg.dependencies || {});
|
|
202
|
-
const newDeps = Object.keys(newPkg.dependencies || {});
|
|
203
|
-
const added = newDeps.filter(d => !oldDeps.includes(d));
|
|
204
|
-
added.forEach(d => console.log('new runtime dep: ' + d));
|
|
205
|
-
" 2>/dev/null || true
|
|
206
|
-
}
|