@khoavu882/doflow 2.0.0-beta.3 → 2.0.0-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/doflow.js +2 -2
- package/core/.antigravity-plugin/plugin.json +0 -1
- package/core/.claude-plugin/plugin.json +1 -1
- package/core/.codex-plugin/plugin.json +1 -1
- package/core/.plugin/plugin.json +1 -1
- package/core/harnesses/claude/hooks/mcp-tool-guard.sh +7 -17
- package/core/harnesses/claude/hooks/pre-bash-guard.sh +7 -17
- package/core/harnesses/claude/hooks/pre-implement-gate.sh +7 -18
- package/core/harnesses/claude/hooks/session-start.sh +5 -3
- package/core/harnesses/claude/hooks/stop-check.sh +5 -3
- package/core/harnesses/claude/hooks/subagent-audit.sh +5 -5
- package/core/harnesses/claude/hooks/user-prompt-submit.sh +5 -3
- package/core/harnesses/codex/hooks/mcp-tool-guard.sh +7 -15
- package/core/harnesses/codex/hooks/pre-bash-guard.sh +7 -15
- package/core/harnesses/codex/hooks/pre-implement-gate.sh +7 -16
- package/core/harnesses/codex/hooks/session-start.sh +5 -3
- package/core/harnesses/codex/hooks/stop-check.sh +6 -5
- package/core/harnesses/codex/hooks/subagent-audit.sh +17 -6
- package/core/harnesses/codex/hooks/user-prompt-submit.sh +5 -4
- package/core/harnesses/gemini/hooks/mcp-tool-guard.sh +7 -18
- package/core/harnesses/gemini/hooks/pre-bash-guard.sh +7 -18
- package/core/harnesses/gemini/hooks/pre-implement-gate.sh +7 -19
- package/core/harnesses/gemini/hooks/session-start.sh +5 -3
- package/core/harnesses/kiro/hooks/mcp-tool-guard.sh +20 -5
- package/core/harnesses/kiro/hooks/pre-bash-guard.sh +20 -5
- package/core/harnesses/kiro/hooks/pre-implement-gate.sh +20 -5
- package/core/harnesses/kiro/hooks/session-start.sh +11 -5
- package/core/harnesses/kiro/hooks/stop-check.sh +21 -30
- package/core/harnesses/pi/extensions/doflow-guard.js +159 -0
- package/core/harnesses/shared/hooks/ENVELOPE_MIGRATION.md +27 -11
- package/core/harnesses/shared/hooks/adapters/antigravity.js +14 -57
- package/core/harnesses/shared/hooks/bridge/bridge.sh +184 -0
- package/core/harnesses/shared/hooks/bridge/envelope.jq +60 -0
- package/core/harnesses/shared/hooks/bridge/respond.sh +62 -0
- package/core/harnesses/shared/hooks/policies/capture-failure.sh +4 -2
- package/core/harnesses/shared/hooks/policies/contract.sh +75 -0
- package/core/harnesses/shared/hooks/policies/deny-json.sh +39 -4
- package/core/harnesses/shared/hooks/policies/lib.sh +4 -2
- package/core/harnesses/shared/hooks/policies/mcp-tool-guard.sh +46 -34
- package/core/harnesses/shared/hooks/policies/pre-bash-guard.sh +75 -25
- package/core/harnesses/shared/hooks/policies/pre-implementation-gate.sh +67 -73
- package/core/harnesses/shared/hooks/policies/session-context.sh +37 -7
- package/core/harnesses/shared/hooks/policies/stop-check.sh +42 -15
- package/core/harnesses/shared/hooks/policies/subagent-audit.sh +35 -5
- package/core/harnesses/shared/hooks/policies/user-prompt-submit.sh +40 -7
- package/core/harnesses/shared/hooks/stream-hook-runner.js +69 -181
- package/core/registry/assets.json +19 -0
- package/core/registry/contracts.json +2 -2
- package/core/registry/harnesses.json +196 -10
- package/core/registry/lifecycle.json +2626 -7
- package/core/registry/storage.json +110 -83
- package/core/registry/verification.json +1 -1
- package/core/shared/scripts/doflow/bash/render-puml.sh +2 -2
- package/core/shared/scripts/doflow/bin/doflow-run +10 -6
- package/core/shared/skills/do-design/SKILL.md +6 -6
- package/core/shared/skills/do-execute-plan/SKILL.md +11 -10
- package/core/shared/skills/do-execute-plan/references/readiness_gate.md +2 -0
- package/core/shared/skills/do-execute-plan/references/scaffold.md +2 -2
- package/core/shared/skills/do-implement/SKILL.md +3 -2
- package/core/shared/skills/do-plan/SKILL.md +7 -8
- package/package.json +4 -2
- package/src/adapters/antigravity/{index.ts → index.js} +141 -105
- package/src/adapters/claude/{index.ts → index.js} +63 -49
- package/src/adapters/claude/{mcp.ts → mcp.js} +16 -16
- package/src/adapters/codex/{agents.ts → agents.js} +24 -24
- package/src/adapters/codex/{config.ts → config.js} +49 -49
- package/src/adapters/codex/{hooks.ts → hooks.js} +62 -27
- package/src/adapters/codex/{index.ts → index.js} +148 -105
- package/src/adapters/codex/{mcp.ts → mcp.js} +38 -38
- package/src/adapters/codex/{native-projection.ts → native-projection.js} +5 -5
- package/src/adapters/copilot/{index.ts → index.js} +42 -42
- package/src/adapters/{copy-tree.ts → copy-tree.js} +74 -74
- package/src/adapters/gemini/{hooks.ts → hooks.js} +78 -37
- package/src/adapters/gemini/{index.ts → index.js} +96 -57
- package/src/adapters/{hook-commands.ts → hook-commands.js} +8 -8
- package/src/adapters/{index.ts → index.js} +13 -13
- package/src/adapters/kiro/{index.ts → index.js} +49 -35
- package/src/adapters/{mcp-entries.ts → mcp-entries.js} +55 -55
- package/src/adapters/opencode/{index.ts → index.js} +52 -52
- package/src/adapters/pi/hooks.js +119 -0
- package/src/adapters/pi/{index.ts → index.js} +64 -39
- package/src/adapters/pi/{json-members.ts → json-members.js} +23 -23
- package/src/adapters/pi/{mcp.ts → mcp.js} +70 -70
- package/src/adapters/{resolve-source.ts → resolve-source.js} +1 -1
- package/src/cli/commands/{install.ts → install.js} +25 -25
- package/src/cli/commands/{list-backups.ts → list-backups.js} +4 -4
- package/src/cli/commands/{reconcile.ts → reconcile.js} +19 -19
- package/src/cli/commands/{remove.ts → remove.js} +10 -10
- package/src/cli/commands/{rollback.ts → rollback.js} +17 -17
- package/src/cli/commands/{self-update.ts → self-update.js} +3 -3
- package/src/cli/commands/{status.ts → status.js} +24 -24
- package/src/cli/commands/{tools.ts → tools.js} +16 -16
- package/src/cli/commands/{update.ts → update.js} +20 -20
- package/src/cli/{index.ts → index.js} +43 -31
- package/src/cli/{runtime-commands.ts → runtime-commands.js} +97 -58
- package/src/cli/{shared.ts → shared.js} +40 -40
- package/src/helper/{bash-helper.ts → bash-helper.js} +7 -7
- package/src/helper/{git.ts → git.js} +2 -2
- package/src/helper/{guardrails.ts → guardrails.js} +10 -10
- package/src/helper/{harness-paths.ts → harness-paths.js} +24 -24
- package/src/helper/{marker-merge.ts → marker-merge.js} +4 -4
- package/src/helper/{prompt.ts → prompt.js} +5 -5
- package/src/helper/{repo-root.ts → repo-root.js} +2 -2
- package/src/helper/{settings-merge.ts → settings-merge.js} +27 -27
- package/src/helper/{settings-scope.ts → settings-scope.js} +3 -3
- package/src/helper/{toml.ts → toml.js} +48 -48
- package/src/install/{backup-set.ts → backup-set.js} +16 -16
- package/src/install/{backup.ts → backup.js} +65 -65
- package/src/install/{context.ts → context.js} +15 -15
- package/src/install/{manifest.ts → manifest.js} +27 -27
- package/src/install/{mcp.ts → mcp.js} +30 -30
- package/src/install/{paths.ts → paths.js} +15 -15
- package/src/install/{targets.ts → targets.js} +10 -10
- package/src/install/{tool-lifecycle.ts → tool-lifecycle.js} +20 -20
- package/src/lifecycle/{bash-availability.ts → bash-availability.js} +1 -1
- package/src/lifecycle/coverage.js +57 -0
- package/src/lifecycle/hook-step.js +61 -0
- package/src/lifecycle/{index.ts → index.js} +160 -146
- package/src/lifecycle/{mcp-index.ts → mcp-index.js} +1 -1
- package/src/lifecycle/{policies.ts → policies.js} +11 -11
- package/src/lifecycle/{view.ts → view.js} +28 -28
- package/src/registry/{index.ts → index.js} +195 -66
- package/src/release/build-package.js +91 -0
- package/src/release/changelog-section.js +70 -0
- package/src/release/{sync-plugin-versions.ts → sync-plugin-versions.js} +4 -4
- package/src/runtime/advice/{cli.ts → cli.js} +31 -31
- package/src/runtime/advice/{client.ts → client.js} +44 -44
- package/src/runtime/advice/{config.ts → config.js} +16 -16
- package/src/runtime/advice/{doctor.ts → doctor.js} +32 -32
- package/src/runtime/advice/{endpoint-guard.ts → endpoint-guard.js} +20 -20
- package/src/runtime/advice/{kinds.ts → kinds.js} +16 -16
- package/src/runtime/advice/providers/{ollama-systemone.ts → ollama-systemone.js} +21 -21
- package/src/runtime/advice/{record.ts → record.js} +19 -19
- package/src/runtime/advice/{registry.ts → registry.js} +16 -16
- package/src/runtime/advice/{strength.ts → strength.js} +7 -7
- package/src/runtime/{brief-tools.ts → brief-tools.js} +50 -50
- package/src/runtime/{c4-project.ts → c4-project.js} +27 -27
- package/src/runtime/candidate.js +505 -0
- package/src/runtime/{capability-router.ts → capability-router.js} +39 -39
- package/src/runtime/{checkouts.ts → checkouts.js} +21 -21
- package/src/runtime/{claims.ts → claims.js} +44 -35
- package/src/runtime/{cli-result.ts → cli-result.js} +15 -8
- package/src/runtime/{cli.ts → cli.js} +59 -292
- package/src/runtime/{command-detect.ts → command-detect.js} +47 -47
- package/src/runtime/context/budget.js +203 -0
- package/src/runtime/context/compose.js +1367 -0
- package/src/runtime/context/delivery-store.js +102 -0
- package/src/runtime/context/mandatory.js +594 -0
- package/src/runtime/context/pointer.js +142 -0
- package/src/runtime/context-pack.js +1277 -0
- package/src/runtime/{decision-register.ts → decision-register.js} +67 -67
- package/src/runtime/evidence-fields.js +39 -0
- package/src/runtime/{evidence-ledger.ts → evidence-ledger.js} +32 -25
- package/src/runtime/failure/{capture.ts → capture.js} +29 -29
- package/src/runtime/failure/{classifier.ts → classifier.js} +4 -4
- package/src/runtime/failure/{cli.ts → cli.js} +30 -30
- package/src/runtime/failure/{home.ts → home.js} +6 -6
- package/src/runtime/failure/{store.ts → store.js} +32 -32
- package/src/runtime/{feature-resolve.ts → feature-resolve.js} +6 -6
- package/src/runtime/{freshness.ts → freshness.js} +59 -22
- package/src/runtime/{health.ts → health.js} +72 -72
- package/src/runtime/{history-compactor.ts → history-compactor.js} +25 -25
- package/src/runtime/{implementation-gate.ts → implementation-gate.js} +30 -30
- package/src/runtime/inventory/{coverage.ts → coverage.js} +10 -10
- package/src/runtime/inventory/{identity.ts → identity.js} +2 -2
- package/src/runtime/inventory/{index.ts → index.js} +36 -36
- package/src/runtime/inventory/{precedence.ts → precedence.js} +2 -2
- package/src/runtime/inventory/{read-scopes.ts → read-scopes.js} +9 -9
- package/src/runtime/inventory/{siblings.ts → siblings.js} +15 -15
- package/src/runtime/knowledge/{chunker.ts → chunker.js} +12 -12
- package/src/runtime/knowledge/{hybrid.ts → hybrid.js} +17 -17
- package/src/runtime/knowledge/{index-store.ts → index-store.js} +23 -23
- package/src/runtime/knowledge/{retrieval.ts → retrieval.js} +27 -27
- package/src/runtime/{leak-scan.ts → leak-scan.js} +12 -12
- package/src/runtime/lifecycle/{cli.ts → cli.js} +32 -32
- package/src/runtime/lifecycle/{event-store.ts → event-store.js} +48 -48
- package/src/runtime/lifecycle/{fold.ts → fold.js} +41 -41
- package/src/runtime/lifecycle/{followup.ts → followup.js} +54 -54
- package/src/runtime/lifecycle/{goal.ts → goal.js} +50 -50
- package/src/runtime/lifecycle/{intent-writer.ts → intent-writer.js} +5 -5
- package/src/runtime/lifecycle/{overview.ts → overview.js} +47 -47
- package/src/runtime/lifecycle/{release.ts → release.js} +29 -29
- package/src/runtime/lifecycle/{report-store.ts → report-store.js} +25 -25
- package/src/runtime/lifecycle/{retention.ts → retention.js} +37 -37
- package/src/runtime/lifecycle/{root.ts → root.js} +9 -9
- package/src/runtime/lifecycle/{status.ts → status.js} +64 -64
- package/src/runtime/lifecycle/{store-upkeep.ts → store-upkeep.js} +46 -46
- package/src/runtime/{locator-resolve.ts → locator-resolve.js} +7 -7
- package/src/runtime/{mask.ts → mask.js} +22 -22
- package/src/runtime/{mcp-consultations.ts → mcp-consultations.js} +54 -54
- package/src/runtime/{mcp-reach.ts → mcp-reach.js} +24 -24
- package/src/runtime/{model-router.ts → model-router.js} +20 -20
- package/src/runtime/{outcome.ts → outcome.js} +27 -27
- package/src/runtime/{reach.ts → reach.js} +21 -21
- package/src/runtime/{readiness-record.ts → readiness-record.js} +12 -12
- package/src/runtime/{readiness.ts → readiness.js} +113 -43
- package/src/runtime/{recovery.ts → recovery.js} +15 -15
- package/src/runtime/{research-request.ts → research-request.js} +34 -34
- package/src/runtime/{retrieval-plan.ts → retrieval-plan.js} +453 -104
- package/src/runtime/retrieval-record.js +223 -0
- package/src/runtime/scaffold/{artifacts.ts → artifacts.js} +22 -19
- package/src/runtime/scaffold/{fingerprint.ts → fingerprint.js} +3 -3
- package/src/runtime/scaffold/{generate.ts → generate.js} +109 -109
- package/src/runtime/scaffold/{languages.ts → languages.js} +34 -34
- package/src/runtime/source-version.js +289 -0
- package/src/runtime/storage/{cli.ts → cli.js} +9 -9
- package/src/runtime/storage/{engine.ts → engine.js} +37 -37
- package/src/runtime/storage/{registry.ts → registry.js} +19 -19
- package/src/runtime/storage/{roots.ts → roots.js} +8 -8
- package/src/runtime/storage/{rules.ts → rules.js} +26 -26
- package/src/runtime/storage/{upkeep.ts → upkeep.js} +35 -35
- package/src/runtime/storage/{work-status.ts → work-status.js} +39 -39
- package/src/runtime/{task-attempts.ts → task-attempts.js} +62 -62
- package/src/runtime/{task-classifier.ts → task-classifier.js} +34 -34
- package/src/runtime/{task-scope.ts → task-scope.js} +40 -15
- package/src/runtime/{task-state.ts → task-state.js} +86 -21
- package/src/runtime/trace/{indicators.ts → indicators.js} +22 -22
- package/src/runtime/trace/{ledger.ts → ledger.js} +42 -42
- package/src/runtime/trace/{render.ts → render.js} +7 -7
- package/src/runtime/trace/{views.ts → views.js} +40 -40
- package/src/runtime/verification/{contract-runner.ts → contract-runner.js} +23 -23
- package/src/runtime/verification/{engine.ts → engine.js} +143 -143
- package/src/runtime/verification/{registry.ts → registry.js} +6 -6
- package/src/runtime/verification/{scope-bound.ts → scope-bound.js} +50 -50
- package/src/runtime/verification/{test-selection.ts → test-selection.js} +28 -28
- package/src/runtime/{workflow-engine.ts → workflow-engine.js} +50 -50
- package/src/runtime/{workflow-orchestrator.ts → workflow-orchestrator.js} +91 -91
- package/src/runtime/{worktree.ts → worktree.js} +31 -31
- package/src/state/{index.ts → index.js} +26 -26
- package/src/state/{lockfile.ts → lockfile.js} +20 -20
- package/src/state/{run-lock.ts → run-lock.js} +44 -44
- package/core/.antigravity-plugin/hooks.json +0 -39
- package/core/harnesses/shared/hooks/adapters/arg-fields.js +0 -19
- package/core/harnesses/shared/hooks/adapters/gemini.js +0 -114
- package/core/harnesses/shared/hooks/hooks.json +0 -47
- package/src/runtime/context-pack.ts +0 -492
|
@@ -17,19 +17,19 @@
|
|
|
17
17
|
|
|
18
18
|
const SETTLE_AS = ['kept', 'dismissed', 'fix', 'done'];
|
|
19
19
|
|
|
20
|
-
function byOrder(a
|
|
20
|
+
function byOrder(a , b ) {
|
|
21
21
|
const delta = Date.parse(a.at) - Date.parse(b.at);
|
|
22
22
|
if (delta !== 0) return delta;
|
|
23
23
|
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
function isText(value
|
|
27
|
-
function isIdList(value
|
|
26
|
+
function isText(value ) { return typeof value === 'string' && value.trim() !== ''; }
|
|
27
|
+
function isIdList(value ) { return Array.isArray(value) && value.length > 0 && value.every(isText); }
|
|
28
28
|
|
|
29
29
|
/** DEC-045: an event dated further ahead than this stays out of the fold and is listed as a conflict. */
|
|
30
30
|
const FUTURE_LIMIT_MS = 24 * 60 * 60 * 1000;
|
|
31
31
|
|
|
32
|
-
function newState({ now = new Date() }
|
|
32
|
+
function newState({ now = new Date() } = {}) {
|
|
33
33
|
return {
|
|
34
34
|
cutoff: now.getTime() + FUTURE_LIMIT_MS,
|
|
35
35
|
items: new Map(),
|
|
@@ -43,15 +43,15 @@ function newState({ now = new Date() }: { now?: Date } = {}): any {
|
|
|
43
43
|
};
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
function conflict(event
|
|
46
|
+
function conflict(event , code , reason ) {
|
|
47
47
|
return { event: event.id, type: event.type, reason, code };
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
function touch(item
|
|
50
|
+
function touch(item , event ) {
|
|
51
51
|
item.history.push({ event: event.id, type: event.type, at: event.at, by: event.by });
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
function itemFor(state
|
|
54
|
+
function itemFor(state , event , id ) {
|
|
55
55
|
const item = state.items.get(id);
|
|
56
56
|
if (!item) return { problem: conflict(event, 'unknown-id', `${id} is not a follow-up`) };
|
|
57
57
|
if (state.final.has(id)) return { problem: conflict(event, 'illegal-transition', `${id} is done and accepts no later event`) };
|
|
@@ -60,13 +60,13 @@ function itemFor(state: any, event: any, id: string): { item?: any; problem?: an
|
|
|
60
60
|
|
|
61
61
|
// ── follow-ups ─────────────────────────────────────────────────────────────────────────────────
|
|
62
62
|
|
|
63
|
-
function applyAdded(state
|
|
63
|
+
function applyAdded(state , event ) {
|
|
64
64
|
const d = event.data;
|
|
65
65
|
if (!isText(d.id) || !isText(d.statement) || !d.source || typeof d.source !== 'object' || !isText(d.source.kind)) {
|
|
66
66
|
return [conflict(event, 'malformed', 'followup.added needs id, statement and source.kind')];
|
|
67
67
|
}
|
|
68
68
|
if (state.items.has(d.id)) return [conflict(event, 'illegal-transition', `${d.id} was already added`)];
|
|
69
|
-
const item
|
|
69
|
+
const item = {
|
|
70
70
|
id: d.id,
|
|
71
71
|
statement: d.statement,
|
|
72
72
|
source: d.source,
|
|
@@ -88,10 +88,10 @@ function applyAdded(state: any, event: any): any[] {
|
|
|
88
88
|
return [];
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
function applyTaken(state
|
|
91
|
+
function applyTaken(state , event ) {
|
|
92
92
|
const d = event.data;
|
|
93
93
|
if (!isIdList(d.ids) || !isText(d.feature)) return [conflict(event, 'malformed', 'followup.taken needs ids and feature')];
|
|
94
|
-
const problems
|
|
94
|
+
const problems = [];
|
|
95
95
|
for (const id of d.ids) {
|
|
96
96
|
const { item, problem } = itemFor(state, event, id);
|
|
97
97
|
if (problem) { problems.push(problem); continue; }
|
|
@@ -104,11 +104,11 @@ function applyTaken(state: any, event: any): any[] {
|
|
|
104
104
|
return problems;
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
function settleOne(state
|
|
107
|
+
function settleOne(state , event , id ) {
|
|
108
108
|
const d = event.data;
|
|
109
109
|
const { item, problem } = itemFor(state, event, id);
|
|
110
110
|
if (problem) return problem;
|
|
111
|
-
const refuse = (reason
|
|
111
|
+
const refuse = (reason ) => conflict(event, 'illegal-transition', reason);
|
|
112
112
|
switch (d.as) {
|
|
113
113
|
case 'kept':
|
|
114
114
|
// IC-003: a reason is required when a taken item is released or a dismissed one reopens, not for an open one.
|
|
@@ -139,18 +139,18 @@ function settleOne(state: any, event: any, id: string): any {
|
|
|
139
139
|
return null;
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
-
function applySettled(state
|
|
142
|
+
function applySettled(state , event ) {
|
|
143
143
|
const d = event.data;
|
|
144
144
|
// The legacy single-id shape (`id`) and the batch shape (`ids`) both fold; the verb writes one event per id.
|
|
145
145
|
const ids = isText(d.id) ? [d.id] : d.ids;
|
|
146
146
|
if (!isIdList(ids)) return [conflict(event, 'malformed', 'followup.settled needs an id')];
|
|
147
|
-
return ids.map((id
|
|
147
|
+
return ids.map((id ) => settleOne(state, event, id)).filter(Boolean);
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
-
function applyPromoted(state
|
|
150
|
+
function applyPromoted(state , event ) {
|
|
151
151
|
const d = event.data;
|
|
152
152
|
if (!isIdList(d.ids) || !isText(d.intent)) return [conflict(event, 'malformed', 'followup.promoted needs ids and intent')];
|
|
153
|
-
const problems
|
|
153
|
+
const problems = [];
|
|
154
154
|
for (const id of d.ids) {
|
|
155
155
|
const { item, problem } = itemFor(state, event, id);
|
|
156
156
|
if (problem) { problems.push(problem); continue; }
|
|
@@ -164,7 +164,7 @@ function applyPromoted(state: any, event: any): any[] {
|
|
|
164
164
|
|
|
165
165
|
// ── features, goals, releases ──────────────────────────────────────────────────────────────────
|
|
166
166
|
|
|
167
|
-
function applyTracked(state
|
|
167
|
+
function applyTracked(state , event ) {
|
|
168
168
|
const slug = event.data.slug;
|
|
169
169
|
if (!isText(slug)) return [conflict(event, 'malformed', 'feature.tracked needs a slug')];
|
|
170
170
|
// The earliest event counts; a later one from another clone changes nothing and is no conflict.
|
|
@@ -172,19 +172,19 @@ function applyTracked(state: any, event: any): any[] {
|
|
|
172
172
|
return [];
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
-
function applyMerged(state
|
|
175
|
+
function applyMerged(state , event ) {
|
|
176
176
|
const { slug, reason } = event.data;
|
|
177
177
|
if (!isText(slug)) return [conflict(event, 'malformed', 'feature.merged needs a slug')];
|
|
178
178
|
if (!state.merged.has(slug)) state.merged.set(slug, { slug, reason: typeof reason === 'string' ? reason : '', at: event.at });
|
|
179
179
|
return [];
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
-
function goalFor(state
|
|
182
|
+
function goalFor(state , event , id ) {
|
|
183
183
|
const goal = state.goals.get(id);
|
|
184
184
|
return goal ? { goal } : { problem: conflict(event, 'unknown-goal', `${id} is not a goal`) };
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
-
function applyGoalAdded(state
|
|
187
|
+
function applyGoalAdded(state , event ) {
|
|
188
188
|
const d = event.data;
|
|
189
189
|
if (!isText(d.goal) || !Array.isArray(d.items)) return [conflict(event, 'malformed', 'goal.added needs goal and items')];
|
|
190
190
|
// A second `goal.added` for one id, as another clone may write, loses: the first wins.
|
|
@@ -195,26 +195,26 @@ function applyGoalAdded(state: any, event: any): any[] {
|
|
|
195
195
|
status: 'open',
|
|
196
196
|
reason: null,
|
|
197
197
|
added: event.at.slice(0, 10),
|
|
198
|
-
items: d.items.filter((i
|
|
198
|
+
items: d.items.filter((i ) => i && isText(i.id)).map((i ) => ({ id: i.id, text: String(i.text ?? ''), met: false, evidence: null })),
|
|
199
199
|
});
|
|
200
200
|
return [];
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
-
function applyGoalItemAdded(state
|
|
203
|
+
function applyGoalItemAdded(state , event ) {
|
|
204
204
|
const d = event.data;
|
|
205
205
|
const { goal, problem } = goalFor(state, event, d.goal);
|
|
206
206
|
if (problem) return [problem];
|
|
207
207
|
if (!d.item || !isText(d.item.id)) return [conflict(event, 'malformed', 'goal.item-added needs item.id')];
|
|
208
|
-
if (goal.items.some((i
|
|
208
|
+
if (goal.items.some((i ) => i.id === d.item.id)) return [conflict(event, 'illegal-transition', `${d.goal} already has item ${d.item.id}`)];
|
|
209
209
|
goal.items.push({ id: d.item.id, text: String(d.item.text ?? ''), met: false, evidence: null });
|
|
210
210
|
return [];
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
-
function applyGoalChecked(state
|
|
213
|
+
function applyGoalChecked(state , event ) {
|
|
214
214
|
const d = event.data;
|
|
215
215
|
const { goal, problem } = goalFor(state, event, d.goal);
|
|
216
216
|
if (problem) return [problem];
|
|
217
|
-
const item = goal.items.find((i
|
|
217
|
+
const item = goal.items.find((i ) => i.id === d.item);
|
|
218
218
|
if (!item) return [conflict(event, 'unknown-item', `${d.goal} has no item ${d.item}`)];
|
|
219
219
|
if (!isText(d.evidence)) return [conflict(event, 'evidence-required', 'goal.checked needs evidence')];
|
|
220
220
|
item.met = d.met === true;
|
|
@@ -222,7 +222,7 @@ function applyGoalChecked(state: any, event: any): any[] {
|
|
|
222
222
|
return [];
|
|
223
223
|
}
|
|
224
224
|
|
|
225
|
-
function applyGoalLinked(state
|
|
225
|
+
function applyGoalLinked(state , event ) {
|
|
226
226
|
const d = event.data;
|
|
227
227
|
const { problem } = goalFor(state, event, d.goal);
|
|
228
228
|
if (problem) return [problem];
|
|
@@ -235,7 +235,7 @@ function applyGoalLinked(state: any, event: any): any[] {
|
|
|
235
235
|
return [];
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
-
function applyGoalDone(state
|
|
238
|
+
function applyGoalDone(state , event ) {
|
|
239
239
|
const d = event.data;
|
|
240
240
|
const { goal, problem } = goalFor(state, event, d.goal);
|
|
241
241
|
if (problem) return [problem];
|
|
@@ -244,7 +244,7 @@ function applyGoalDone(state: any, event: any): any[] {
|
|
|
244
244
|
return [];
|
|
245
245
|
}
|
|
246
246
|
|
|
247
|
-
function applyReleaseRecorded(state
|
|
247
|
+
function applyReleaseRecorded(state , event ) {
|
|
248
248
|
const d = event.data;
|
|
249
249
|
if (!isText(d.tag)) return [conflict(event, 'malformed', 'release.recorded needs a tag')];
|
|
250
250
|
let release = state.releases.get(d.tag);
|
|
@@ -260,7 +260,7 @@ function applyReleaseRecorded(state: any, event: any): any[] {
|
|
|
260
260
|
return [];
|
|
261
261
|
}
|
|
262
262
|
|
|
263
|
-
const APPLY
|
|
263
|
+
const APPLY = {
|
|
264
264
|
'followup.added': applyAdded,
|
|
265
265
|
'followup.taken': applyTaken,
|
|
266
266
|
'followup.settled': applySettled,
|
|
@@ -283,7 +283,7 @@ const EVENT_TYPES = Object.keys(APPLY);
|
|
|
283
283
|
* conflicts to refuse before anything is written.
|
|
284
284
|
* @returns {Array<{event:string,type:string,reason:string,code:string}>} the conflicts this event raised
|
|
285
285
|
*/
|
|
286
|
-
function applyEvent(state
|
|
286
|
+
function applyEvent(state , event , { checkFuture = true } = {}) {
|
|
287
287
|
const at = Date.parse(event.at);
|
|
288
288
|
if (checkFuture && at > state.cutoff) {
|
|
289
289
|
// Left out of the stamping floor and of every tracking bound: one wrong clock must not move them.
|
|
@@ -301,7 +301,7 @@ function applyEvent(state: any, event: any, { checkFuture = true }: { checkFutur
|
|
|
301
301
|
}
|
|
302
302
|
|
|
303
303
|
/** @param {Array<Object>} events IC-002 envelopes, in any order */
|
|
304
|
-
function foldInto(events
|
|
304
|
+
function foldInto(events , options = {}) {
|
|
305
305
|
const state = newState(options);
|
|
306
306
|
if (options.shouldStop?.()) { state.incomplete = true; return state; }
|
|
307
307
|
const ordered = [...events].sort(byOrder);
|
|
@@ -315,16 +315,16 @@ function foldInto(events: any[], options: any = {}): any {
|
|
|
315
315
|
}
|
|
316
316
|
|
|
317
317
|
/** The result of a fold, as plain data. `hasBody(item)` tells a report whose body is on this machine. */
|
|
318
|
-
function finalize(state
|
|
319
|
-
const followups = [...state.items.values()].map((item
|
|
318
|
+
function finalize(state , { hasBody = () => false } = {}) {
|
|
319
|
+
const followups = [...state.items.values()].map((item ) => (
|
|
320
320
|
item.source.kind === 'report' ? { ...item, body: hasBody(item) ? 'on-this-machine' : 'not-on-this-machine' } : item));
|
|
321
|
-
const releases = [...state.releases.values()].map((r
|
|
321
|
+
const releases = [...state.releases.values()].map((r ) => {
|
|
322
322
|
const excluded = [...r.excluded];
|
|
323
|
-
return { tag: r.tag, commit: r.commit, features: [...r.features.values()].filter((f
|
|
323
|
+
return { tag: r.tag, commit: r.commit, features: [...r.features.values()].filter((f ) => !r.excluded.has(f.slug)), excluded };
|
|
324
324
|
});
|
|
325
325
|
return {
|
|
326
326
|
followups,
|
|
327
|
-
features: [...state.tracked.values()].map((f
|
|
327
|
+
features: [...state.tracked.values()].map((f ) => ({ ...f })),
|
|
328
328
|
merged: [...state.merged.values()],
|
|
329
329
|
goals: [...state.goals.values()],
|
|
330
330
|
releases,
|
|
@@ -334,7 +334,7 @@ function finalize(state: any, { hasBody = () => false }: any = {}): any {
|
|
|
334
334
|
}
|
|
335
335
|
|
|
336
336
|
/** @returns {ReturnType<typeof finalize>} */
|
|
337
|
-
function foldEvents(events
|
|
337
|
+
function foldEvents(events , options = {}) {
|
|
338
338
|
return finalize(foldInto(events, options), options);
|
|
339
339
|
}
|
|
340
340
|
|
|
@@ -344,9 +344,9 @@ function foldEvents(events: any[], options: any = {}): any {
|
|
|
344
344
|
* @param {Array<Object>} followups folded items
|
|
345
345
|
* @param {Object<string,string>|Map<string,string>} statuses feature slug to derived status
|
|
346
346
|
*/
|
|
347
|
-
function withDerivedDone(followups
|
|
348
|
-
const statusOf = (slug
|
|
349
|
-
return followups.map((item
|
|
347
|
+
function withDerivedDone(followups , statuses ) {
|
|
348
|
+
const statusOf = (slug ) => (statuses instanceof Map ? statuses.get(slug) : statuses[slug]);
|
|
349
|
+
return followups.map((item ) => (
|
|
350
350
|
item.state === 'taken' && statusOf(item.takenBy) === 'finished' ? { ...item, state: 'done', derived: true } : item));
|
|
351
351
|
}
|
|
352
352
|
|
|
@@ -17,15 +17,15 @@
|
|
|
17
17
|
const nodeFs = require('node:fs');
|
|
18
18
|
const os = require('node:os');
|
|
19
19
|
const path = require('node:path');
|
|
20
|
-
const { maskLine } = require('../mask.
|
|
21
|
-
const { STAGES: CHAIN_STAGES } = require('../decision-register.
|
|
22
|
-
const { isSafeSlug, invalidSlugRefusal } = require('../task-scope.
|
|
23
|
-
const { resolveActiveFeature } = require('../feature-resolve.
|
|
24
|
-
const { SETTLE_AS, withDerivedDone } = require('./fold.
|
|
25
|
-
const { appendEvents, readFold, byFromChannel, randomChars } = require('./event-store.
|
|
26
|
-
const { deriveStatuses } = require('./status.
|
|
27
|
-
const { writeIntent, kebabTitle } = require('./intent-writer.
|
|
28
|
-
const reportStore = require('./report-store.
|
|
20
|
+
const { maskLine } = require('../mask.js');
|
|
21
|
+
const { STAGES: CHAIN_STAGES } = require('../decision-register.js');
|
|
22
|
+
const { isSafeSlug, invalidSlugRefusal } = require('../task-scope.js');
|
|
23
|
+
const { resolveActiveFeature } = require('../feature-resolve.js');
|
|
24
|
+
const { SETTLE_AS, withDerivedDone } = require('./fold.js');
|
|
25
|
+
const { appendEvents, readFold, byFromChannel, randomChars } = require('./event-store.js');
|
|
26
|
+
const { deriveStatuses } = require('./status.js');
|
|
27
|
+
const { writeIntent, kebabTitle } = require('./intent-writer.js');
|
|
28
|
+
const reportStore = require('./report-store.js');
|
|
29
29
|
|
|
30
30
|
/** The decision register's stages plus the two lifecycle stages (IC-006). */
|
|
31
31
|
const STAGES = [...CHAIN_STAGES, 'release', 'maintain'];
|
|
@@ -43,16 +43,16 @@ const CONFLICT_FINDINGS = new Set(['unknown-id', 'untracked-feature', 'intent-ex
|
|
|
43
43
|
/** A caller mistake (exit 2). */
|
|
44
44
|
class FollowupUsageError extends Error {}
|
|
45
45
|
|
|
46
|
-
function refusal(action
|
|
46
|
+
function refusal(action , finding , message ) { return { ok: false, action, finding, message }; }
|
|
47
47
|
|
|
48
48
|
/** Maps an `appendEvents` failure to a refusal result. */
|
|
49
|
-
function refusalFrom(action
|
|
49
|
+
function refusalFrom(action , failed ) {
|
|
50
50
|
const finding = failed.conflict && CONFLICT_FINDINGS.has(failed.conflict.code) ? failed.conflict.code : failed.finding;
|
|
51
51
|
return refusal(action, finding, failed.message);
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
/** Masks a free-text field and checks it is one non-empty line of at most `max` characters. */
|
|
55
|
-
function oneLine(raw
|
|
55
|
+
function oneLine(raw , label , problems , { max = STATEMENT_MAX } = {}) {
|
|
56
56
|
// The length is checked before masking as well, so the masking patterns never see more than a bounded input.
|
|
57
57
|
const rawLength = String(raw ?? '').length;
|
|
58
58
|
if (rawLength > max * 4) { problems.push(`${label} is ${rawLength} characters; the limit is ${max}`); return null; }
|
|
@@ -64,20 +64,20 @@ function oneLine(raw: any, label: any, problems: any, { max = STATEMENT_MAX }: a
|
|
|
64
64
|
return text;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
function newFollowupId(taken
|
|
67
|
+
function newFollowupId(taken ) {
|
|
68
68
|
for (;;) {
|
|
69
69
|
const id = `FU-${randomChars(6)}`;
|
|
70
70
|
if (!taken.has(id)) { taken.add(id); return id; }
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
function channelBy(channel
|
|
74
|
+
function channelBy(channel ) {
|
|
75
75
|
const by = byFromChannel(channel || 'default');
|
|
76
76
|
if (!by) throw new FollowupUsageError(`unknown --channel '${channel}'. Valid: question, gate, prompt, default`);
|
|
77
77
|
return by;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
function parseIds(raw
|
|
80
|
+
function parseIds(raw , label = '--ids') {
|
|
81
81
|
const list = Array.isArray(raw) ? raw : String(raw ?? '').split(',');
|
|
82
82
|
const ids = [...new Set(list.flatMap((entry) => String(entry).split(',')).map((id) => id.trim()).filter(Boolean))];
|
|
83
83
|
if (ids.length === 0) throw new FollowupUsageError(`${label} is required: one or more follow-up ids, comma separated`);
|
|
@@ -87,18 +87,18 @@ function parseIds(raw: any, label = '--ids') {
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
/** The folded store, with a taken item shown `done` while its feature derives finished (IC-003, IC-021). */
|
|
90
|
-
function loadFollowups(root
|
|
90
|
+
function loadFollowups(root , { hasBody, fsImpl = nodeFs, statuses, now = new Date() } = {}) {
|
|
91
91
|
const fold = readFold(root, { hasBody: hasBody || reportStore.bodyChecker(root, { fsImpl }), fsImpl, now });
|
|
92
|
-
const needsStatus = fold.followups.some((item
|
|
92
|
+
const needsStatus = fold.followups.some((item ) => item.state === 'taken');
|
|
93
93
|
const derived = needsStatus ? (statuses || deriveStatuses({ root, fold })) : null;
|
|
94
|
-
const followups = needsStatus ? withDerivedDone(fold.followups, Object.fromEntries(Object.entries(derived.statuses).map(([slug, s]
|
|
94
|
+
const followups = needsStatus ? withDerivedDone(fold.followups, Object.fromEntries(Object.entries(derived.statuses).map(([slug, s] ) => [slug, s.status]))) : fold.followups;
|
|
95
95
|
return { fold, followups };
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
// ── add ────────────────────────────────────────────────────────────────────────────────────────
|
|
99
99
|
|
|
100
100
|
/** The active feature slug: `--slug`, else the branch through the existing resolver; null when neither. */
|
|
101
|
-
function resolveFeatureSlug({ cwd, slug }
|
|
101
|
+
function resolveFeatureSlug({ cwd, slug } ) {
|
|
102
102
|
if (slug) {
|
|
103
103
|
const bad = invalidSlugRefusal(slug);
|
|
104
104
|
if (bad) throw new FollowupUsageError(bad.message);
|
|
@@ -118,8 +118,8 @@ function resolveFeatureSlug({ cwd, slug }: any) {
|
|
|
118
118
|
return resolved;
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
function sourceFor(kind
|
|
122
|
-
const word = (value
|
|
121
|
+
function sourceFor(kind , fields , label , problems ) {
|
|
122
|
+
const word = (value , name ) => {
|
|
123
123
|
if (typeof value === 'string' && WORD.test(value)) return value;
|
|
124
124
|
problems.push(`${label}: ${name} must be letters, digits, dot, underscore or dash`);
|
|
125
125
|
return null;
|
|
@@ -145,15 +145,15 @@ function sourceFor(kind: any, fields: any, label: any, problems: any) {
|
|
|
145
145
|
* Validates every item and builds its `followup.added` data; nothing is written unless all pass.
|
|
146
146
|
* @returns {Array<{statement:string, source:Object}>}
|
|
147
147
|
*/
|
|
148
|
-
function buildItems({ cwd, items, defaults }
|
|
149
|
-
const problems
|
|
150
|
-
let featureSlug
|
|
148
|
+
function buildItems({ cwd, items, defaults } ) {
|
|
149
|
+
const problems = [];
|
|
150
|
+
let featureSlug ;
|
|
151
151
|
const feature = () => {
|
|
152
152
|
if (featureSlug === undefined) featureSlug = resolveFeatureSlug({ cwd, slug: defaults.slug });
|
|
153
153
|
return featureSlug;
|
|
154
154
|
};
|
|
155
|
-
const built
|
|
156
|
-
items.forEach((raw
|
|
155
|
+
const built = [];
|
|
156
|
+
items.forEach((raw , index ) => {
|
|
157
157
|
const label = items.length > 1 || defaults.batch ? `item ${index + 1}` : 'the follow-up';
|
|
158
158
|
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) { problems.push(`${label} must be an object`); return; }
|
|
159
159
|
const statement = oneLine(raw.statement, `${label}: statement`, problems);
|
|
@@ -194,7 +194,7 @@ function buildItems({ cwd, items, defaults }: any) {
|
|
|
194
194
|
* must be a regular file of at most 16 MiB, so a device or a FIFO is refused, never read.
|
|
195
195
|
* @returns {Object[]}
|
|
196
196
|
*/
|
|
197
|
-
function readBatchFile(file
|
|
197
|
+
function readBatchFile(file , fsImpl = nodeFs) {
|
|
198
198
|
let parsed;
|
|
199
199
|
let fd;
|
|
200
200
|
try {
|
|
@@ -205,7 +205,7 @@ function readBatchFile(file: any, fsImpl = nodeFs) {
|
|
|
205
205
|
if (!st.isFile()) throw new Error('not a regular file');
|
|
206
206
|
if (st.size > reportStore.MAX_READ_BYTES) throw new Error('larger than 16 MiB');
|
|
207
207
|
parsed = JSON.parse(fsImpl.readFileSync(fd, 'utf8'));
|
|
208
|
-
} catch (error
|
|
208
|
+
} catch (error ) {
|
|
209
209
|
throw new FollowupUsageError(`cannot read --batch ${file}: ${error.message}`);
|
|
210
210
|
} finally {
|
|
211
211
|
if (fd !== undefined) { try { fsImpl.closeSync(fd); } catch { /* read already done */ } }
|
|
@@ -218,18 +218,18 @@ function readBatchFile(file: any, fsImpl = nodeFs) {
|
|
|
218
218
|
* IC-006 `add`. `items` are `{statement, stage?, source?}`; `defaults` holds the flag values
|
|
219
219
|
* (`stage`, `slug`, `source`, `taskClass`, `taskId`, `release`, `batch`).
|
|
220
220
|
*/
|
|
221
|
-
function addFollowups({ root, cwd = process.cwd(), items, defaults = {}, channel, now = new Date(), fsImpl = nodeFs }
|
|
221
|
+
function addFollowups({ root, cwd = process.cwd(), items, defaults = {}, channel, now = new Date(), fsImpl = nodeFs } ) {
|
|
222
222
|
const by = channelBy(channel);
|
|
223
223
|
const built = buildItems({ cwd, items, defaults });
|
|
224
|
-
const taken = new Set(readFold(root, { fsImpl, now }).followups.map((f
|
|
224
|
+
const taken = new Set(readFold(root, { fsImpl, now }).followups.map((f ) => f.id));
|
|
225
225
|
const drafts = built.map(({ statement, source }) => ({ type: 'followup.added', by, data: { id: newFollowupId(taken), statement, source } }));
|
|
226
226
|
const out = appendEvents(root, drafts, { now, fsImpl });
|
|
227
227
|
if (!out.ok) return refusalFrom('add', out);
|
|
228
228
|
return {
|
|
229
229
|
ok: true,
|
|
230
230
|
action: 'add',
|
|
231
|
-
created: out.written.map(({ event }
|
|
232
|
-
events: out.written.map((w
|
|
231
|
+
created: out.written.map(({ event } ) => ({ id: event.data.id, statement: event.data.statement, source: event.data.source, state: 'open' })),
|
|
232
|
+
events: out.written.map((w ) => w.file),
|
|
233
233
|
next: [],
|
|
234
234
|
};
|
|
235
235
|
}
|
|
@@ -237,9 +237,9 @@ function addFollowups({ root, cwd = process.cwd(), items, defaults = {}, channel
|
|
|
237
237
|
// ── report ─────────────────────────────────────────────────────────────────────────────────────
|
|
238
238
|
|
|
239
239
|
/** `--release` and `--feature` of a report: letters, digits, dot, underscore or dash, as for `--source release`. */
|
|
240
|
-
function reportSource({ release, feature }
|
|
240
|
+
function reportSource({ release, feature } ) {
|
|
241
241
|
const problems = [];
|
|
242
|
-
const source
|
|
242
|
+
const source = { kind: 'report' };
|
|
243
243
|
if (release !== undefined && release !== null) {
|
|
244
244
|
if (typeof release === 'string' && WORD.test(release)) source.release = release; else problems.push('--release must be letters, digits, dot, underscore or dash');
|
|
245
245
|
}
|
|
@@ -269,21 +269,21 @@ function reportSource({ release, feature }: any) {
|
|
|
269
269
|
* @param {string} [options.release]
|
|
270
270
|
* @param {string|string[]} [options.feature]
|
|
271
271
|
*/
|
|
272
|
-
function reportFollowup({ root, statement, input, release, feature, channel, now = new Date(), fsImpl = nodeFs, env = process.env }
|
|
272
|
+
function reportFollowup({ root, statement, input, release, feature, channel, now = new Date(), fsImpl = nodeFs, env = process.env } ) {
|
|
273
273
|
const by = channelBy(channel);
|
|
274
274
|
const source = reportSource({ release, feature });
|
|
275
|
-
const problems
|
|
275
|
+
const problems = [];
|
|
276
276
|
const rawStatement = String(statement ?? '');
|
|
277
277
|
const statementMasked = maskLine(rawStatement).masked;
|
|
278
278
|
const cleanStatement = oneLine(rawStatement, '--statement', problems);
|
|
279
279
|
if (problems.length) throw new FollowupUsageError(problems.join('; '));
|
|
280
280
|
let raw;
|
|
281
|
-
try { raw = reportStore.readReportText(input, { fsImpl }); } catch (error
|
|
281
|
+
try { raw = reportStore.readReportText(input, { fsImpl }); } catch (error ) {
|
|
282
282
|
if (error instanceof reportStore.ReportInputError) throw new FollowupUsageError(error.message);
|
|
283
283
|
throw error;
|
|
284
284
|
}
|
|
285
285
|
let body;
|
|
286
|
-
try { body = reportStore.prepareBody(raw, { home: env.HOME || os.homedir() }); } catch (error
|
|
286
|
+
try { body = reportStore.prepareBody(raw, { home: env.HOME || os.homedir() }); } catch (error ) {
|
|
287
287
|
if (error instanceof reportStore.ReportInputError) throw new FollowupUsageError(error.message);
|
|
288
288
|
throw error;
|
|
289
289
|
}
|
|
@@ -293,9 +293,9 @@ function reportFollowup({ root, statement, input, release, feature, channel, now
|
|
|
293
293
|
// ANSI colour and title sequences were removed before masking; what is left is a real control or bidirectional character.
|
|
294
294
|
if (CONTROL.test(excerpt)) throw new FollowupUsageError('the first 2 KB of the report body contain a control or bidirectional-override character; remove it and file the report again');
|
|
295
295
|
|
|
296
|
-
const taken = new Set(readFold(root, { fsImpl, now }).followups.map((f
|
|
296
|
+
const taken = new Set(readFold(root, { fsImpl, now }).followups.map((f ) => f.id));
|
|
297
297
|
let id;
|
|
298
|
-
let stored
|
|
298
|
+
let stored = { ok: false, reason: 'no-home' };
|
|
299
299
|
for (let attempt = 0; attempt < 5; attempt += 1) {
|
|
300
300
|
id = newFollowupId(taken);
|
|
301
301
|
stored = reportStore.writeBody(root, id, body.buffer, { env, fsImpl });
|
|
@@ -309,7 +309,7 @@ function reportFollowup({ root, statement, input, release, feature, channel, now
|
|
|
309
309
|
let out;
|
|
310
310
|
try {
|
|
311
311
|
out = appendEvents(root, [{ type: 'followup.added', by, data }], { now, fsImpl });
|
|
312
|
-
} catch (error
|
|
312
|
+
} catch (error ) {
|
|
313
313
|
if (kept) reportStore.removeBody(stored.file, fsImpl);
|
|
314
314
|
throw error;
|
|
315
315
|
}
|
|
@@ -327,7 +327,7 @@ function reportFollowup({ root, statement, input, release, feature, channel, now
|
|
|
327
327
|
excerptBytes: Buffer.byteLength(excerpt, 'utf8'), bodyBytes: body.buffer.length,
|
|
328
328
|
body: kept ? 'on-this-machine' : 'not-on-this-machine', masked: statementMasked + body.masked,
|
|
329
329
|
}],
|
|
330
|
-
events: out.written.map((w
|
|
330
|
+
events: out.written.map((w ) => w.file),
|
|
331
331
|
next,
|
|
332
332
|
};
|
|
333
333
|
}
|
|
@@ -335,31 +335,31 @@ function reportFollowup({ root, statement, input, release, feature, channel, now
|
|
|
335
335
|
// ── list ───────────────────────────────────────────────────────────────────────────────────────
|
|
336
336
|
|
|
337
337
|
/** IC-006 `list`: `state` is open (default), taken, done, dismissed or all. */
|
|
338
|
-
function listFollowups({ root, state = 'open', hasBody, fsImpl = nodeFs, statuses, now = new Date() }
|
|
338
|
+
function listFollowups({ root, state = 'open', hasBody, fsImpl = nodeFs, statuses, now = new Date() } ) {
|
|
339
339
|
if (state !== 'all' && !STATES.includes(state)) throw new FollowupUsageError(`--state must be one of ${[...STATES, 'all'].join(', ')} (got '${state}')`);
|
|
340
340
|
const { fold, followups } = loadFollowups(root, { hasBody, fsImpl, statuses, now });
|
|
341
|
-
const items = state === 'all' ? followups : followups.filter((item
|
|
341
|
+
const items = state === 'all' ? followups : followups.filter((item ) => item.state === state);
|
|
342
342
|
return { ok: true, action: 'list', state, count: items.length, items, conflicts: fold.conflicts, unreadable: fold.unreadable, unreadableReasons: fold.unreadableReasons, next: [] };
|
|
343
343
|
}
|
|
344
344
|
|
|
345
345
|
// ── take, settle, promote ──────────────────────────────────────────────────────────────────────
|
|
346
346
|
|
|
347
347
|
/** IC-006 `take`: one `followup.taken` event; every id must be open and the feature tracked. */
|
|
348
|
-
function takeFollowups({ root, ids, slug, channel, now = new Date(), fsImpl = nodeFs }
|
|
348
|
+
function takeFollowups({ root, ids, slug, channel, now = new Date(), fsImpl = nodeFs } ) {
|
|
349
349
|
const list = parseIds(ids);
|
|
350
350
|
if (!slug) throw new FollowupUsageError('--slug is required for --action take');
|
|
351
351
|
const bad = invalidSlugRefusal(slug);
|
|
352
352
|
if (bad) throw new FollowupUsageError(bad.message);
|
|
353
353
|
const out = appendEvents(root, [{ type: 'followup.taken', by: channelBy(channel), data: { ids: list, feature: slug } }], { now, fsImpl });
|
|
354
354
|
if (!out.ok) return refusalFrom('take', out);
|
|
355
|
-
return { ok: true, action: 'take', ids: list, feature: slug, events: out.written.map((w
|
|
355
|
+
return { ok: true, action: 'take', ids: list, feature: slug, events: out.written.map((w ) => w.file), next: [] };
|
|
356
356
|
}
|
|
357
357
|
|
|
358
358
|
/** IC-006 `settle`: one `followup.settled` event per id. Each `as` value's required field is in IC-003. */
|
|
359
|
-
function settleFollowups({ root, ids, as, reason, evidence, channel, now = new Date(), fsImpl = nodeFs }
|
|
359
|
+
function settleFollowups({ root, ids, as, reason, evidence, channel, now = new Date(), fsImpl = nodeFs } ) {
|
|
360
360
|
const list = parseIds(ids);
|
|
361
361
|
if (!SETTLE_AS.includes(as)) throw new FollowupUsageError(`--as must be one of ${SETTLE_AS.join(', ')}${as ? ` (got '${as}')` : ''}`);
|
|
362
|
-
const problems
|
|
362
|
+
const problems = [];
|
|
363
363
|
const cleanReason = reason === undefined ? null : oneLine(reason, '--reason', problems);
|
|
364
364
|
const cleanEvidence = evidence === undefined ? null : oneLine(evidence, '--evidence', problems);
|
|
365
365
|
// `kept` needs a reason only when it releases a taken item or reopens a dismissed one; the fold knows the state.
|
|
@@ -368,7 +368,7 @@ function settleFollowups({ root, ids, as, reason, evidence, channel, now = new D
|
|
|
368
368
|
if (problems.length) throw new FollowupUsageError(problems.join('; '));
|
|
369
369
|
const by = channelBy(channel);
|
|
370
370
|
// `list` shows a taken item whose feature finished as `done`, but the fold still holds it `taken`: say what the user was shown.
|
|
371
|
-
const derivedDone = new Map(loadFollowups(root, { fsImpl, now }).followups.filter((item
|
|
371
|
+
const derivedDone = new Map(loadFollowups(root, { fsImpl, now }).followups.filter((item ) => item.derived).map((item ) => [item.id, item.takenBy]));
|
|
372
372
|
if (as !== 'kept') {
|
|
373
373
|
const shown = list.find((id) => derivedDone.has(id));
|
|
374
374
|
if (shown) {
|
|
@@ -384,7 +384,7 @@ function settleFollowups({ root, ids, as, reason, evidence, channel, now = new D
|
|
|
384
384
|
action: 'settle',
|
|
385
385
|
ids: list,
|
|
386
386
|
as,
|
|
387
|
-
events: out.written.map((w
|
|
387
|
+
events: out.written.map((w ) => w.file),
|
|
388
388
|
next: reopened.map((id) => `${id} was shown as done because feature ${derivedDone.get(id)} finished; keeping it released it from that feature, so it is open again`),
|
|
389
389
|
};
|
|
390
390
|
}
|
|
@@ -393,9 +393,9 @@ function settleFollowups({ root, ids, as, reason, evidence, channel, now = new D
|
|
|
393
393
|
* IC-006 `promote` (IC-024): creates a new intent that names the items, then writes one
|
|
394
394
|
* `followup.promoted` event. It never adds items to an existing intent.
|
|
395
395
|
*/
|
|
396
|
-
function promoteFollowups({ root, ids, title, channel, now = new Date(), fsImpl = nodeFs }
|
|
396
|
+
function promoteFollowups({ root, ids, title, channel, now = new Date(), fsImpl = nodeFs } ) {
|
|
397
397
|
const list = parseIds(ids);
|
|
398
|
-
const problems
|
|
398
|
+
const problems = [];
|
|
399
399
|
const cleanTitle = oneLine(title, '--title', problems, { max: 80 });
|
|
400
400
|
if (problems.length) throw new FollowupUsageError(problems.join('; '));
|
|
401
401
|
const by = channelBy(channel);
|
|
@@ -403,7 +403,7 @@ function promoteFollowups({ root, ids, title, channel, now = new Date(), fsImpl
|
|
|
403
403
|
const { followups } = loadFollowups(root, { fsImpl, now });
|
|
404
404
|
const items = [];
|
|
405
405
|
for (const id of list) {
|
|
406
|
-
const item = followups.find((f
|
|
406
|
+
const item = followups.find((f ) => f.id === id);
|
|
407
407
|
if (!item) return refusal('promote', 'unknown-id', `${id} is not a follow-up. Nothing was written.`);
|
|
408
408
|
if (item.state !== 'open') return refusal('promote', 'illegal-transition', `${id} is ${item.state}; only an open item can be promoted. Nothing was written.`);
|
|
409
409
|
if (item.intent) return refusal('promote', 'intent-exists', `${id} was already promoted to ${item.intent}. Nothing was written.`);
|
|
@@ -418,7 +418,7 @@ function promoteFollowups({ root, ids, title, channel, now = new Date(), fsImpl
|
|
|
418
418
|
const out = appendEvents(root, [{ type: 'followup.promoted', by, data: { ids: list, intent: written.path } }], { now, fsImpl });
|
|
419
419
|
if (!out.ok) return refusalFrom('promote', out);
|
|
420
420
|
committed = true;
|
|
421
|
-
return { ok: true, action: 'promote', intent: written.path, ids: list, events: out.written.map((w
|
|
421
|
+
return { ok: true, action: 'promote', intent: written.path, ids: list, events: out.written.map((w ) => w.file), next: [] };
|
|
422
422
|
} finally {
|
|
423
423
|
// The file is the one this call just created exclusively, so removing it undoes a half-done promotion on every failure path.
|
|
424
424
|
if (!committed) { try { fsImpl.rmSync(written.file, { force: true }); } catch { /* the failure being reported stands */ } }
|