@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
|
@@ -16,11 +16,11 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
const nodeFs = require('node:fs');
|
|
19
|
-
const { isSafeSlug, invalidSlugRefusal } = require('../task-scope.
|
|
20
|
-
const { appendEvents, readFold } = require('./event-store.
|
|
21
|
-
const { deriveStatuses } = require('./status.
|
|
22
|
-
const { FollowupUsageError, oneLine, channelBy } = require('./followup.
|
|
23
|
-
const { goalView, goalConflicts, goalConflictNext } = require('./overview.
|
|
19
|
+
const { isSafeSlug, invalidSlugRefusal } = require('../task-scope.js');
|
|
20
|
+
const { appendEvents, readFold } = require('./event-store.js');
|
|
21
|
+
const { deriveStatuses } = require('./status.js');
|
|
22
|
+
const { FollowupUsageError, oneLine, channelBy } = require('./followup.js');
|
|
23
|
+
const { goalView, goalConflicts, goalConflictNext } = require('./overview.js');
|
|
24
24
|
|
|
25
25
|
const GOAL_ID = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
26
26
|
const GOAL_ID_MAX = 40;
|
|
@@ -32,14 +32,14 @@ const USER_CHANNELS = ['question', 'gate', 'prompt'];
|
|
|
32
32
|
/** Fold conflicts that are a refusal under their own name; any other conflict is an `illegal-transition`. */
|
|
33
33
|
const CONFLICT_FINDINGS = new Set(['goal-exists', 'unknown-goal', 'unknown-item', 'goal-already-linked', 'untracked-feature', 'evidence-required']);
|
|
34
34
|
|
|
35
|
-
function refusal(action
|
|
35
|
+
function refusal(action , finding , message ) { return { ok: false, action, finding, message }; }
|
|
36
36
|
|
|
37
|
-
function refusalFrom(action
|
|
37
|
+
function refusalFrom(action , failed ) {
|
|
38
38
|
const finding = failed.conflict && CONFLICT_FINDINGS.has(failed.conflict.code) ? failed.conflict.code : failed.finding;
|
|
39
39
|
return refusal(action, finding, failed.message);
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
function goalIdOf(raw
|
|
42
|
+
function goalIdOf(raw ) {
|
|
43
43
|
if (raw === undefined || raw === null || raw === '') throw new FollowupUsageError('--goal is required: a kebab-case goal id of at most 40 characters');
|
|
44
44
|
const id = String(raw);
|
|
45
45
|
if (id.length > GOAL_ID_MAX || !GOAL_ID.test(id)) {
|
|
@@ -48,7 +48,7 @@ function goalIdOf(raw: any) {
|
|
|
48
48
|
return id;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
function itemIdOf(raw
|
|
51
|
+
function itemIdOf(raw ) {
|
|
52
52
|
const list = Array.isArray(raw) ? raw : raw === undefined ? [] : [raw];
|
|
53
53
|
if (list.length !== 1) throw new FollowupUsageError('--item takes exactly one checklist id such as C3 for --action check');
|
|
54
54
|
if (!ITEM_ID.test(String(list[0]))) throw new FollowupUsageError(`--item must be a checklist id such as C3 (got '${String(list[0]).slice(0, 40)}')`);
|
|
@@ -56,25 +56,25 @@ function itemIdOf(raw: any) {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
/** One masked, one-line, bounded text; a problem is a usage error naming the flag. */
|
|
59
|
-
function textOf(raw
|
|
60
|
-
const problems
|
|
59
|
+
function textOf(raw , label ) {
|
|
60
|
+
const problems = [];
|
|
61
61
|
const text = raw === undefined ? null : oneLine(raw, label, problems);
|
|
62
62
|
if (problems.length) throw new FollowupUsageError(problems.join('; '));
|
|
63
63
|
return text;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
function requiredText(raw
|
|
66
|
+
function requiredText(raw , label ) {
|
|
67
67
|
if (raw === undefined || raw === null) throw new FollowupUsageError(`${label} is required`);
|
|
68
68
|
return textOf(raw, label);
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
function unknownGoal(action
|
|
72
|
-
const known = fold.goals.map((g
|
|
71
|
+
function unknownGoal(action , fold , id ) {
|
|
72
|
+
const known = fold.goals.map((g ) => g.goal);
|
|
73
73
|
return refusal(action, 'unknown-goal', `${id} is not a goal${known.length ? `; the goals are ${known.join(', ')}` : '; none has been added'}. Nothing was written.`);
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
/** The next free `C<n>`: one past the largest number in use, so an id is never reused. */
|
|
77
|
-
function nextItemId(goal
|
|
77
|
+
function nextItemId(goal ) {
|
|
78
78
|
let top = 0;
|
|
79
79
|
for (const item of goal.items) {
|
|
80
80
|
const m = /^C(\d+)$/.exec(item.id);
|
|
@@ -83,51 +83,51 @@ function nextItemId(goal: any) {
|
|
|
83
83
|
return `C${top + 1}`;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
const progressOf = (goal
|
|
86
|
+
const progressOf = (goal ) => ({ met: goal.items.filter((i ) => i.met).length, total: goal.items.length });
|
|
87
87
|
|
|
88
|
-
function checkLine(goalId
|
|
88
|
+
function checkLine(goalId , item ) {
|
|
89
89
|
return `Record a met item with evidence: doflow-run goal --action check --goal ${goalId} --item ${item} --evidence "<what shows it>"`;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
function doneLine(goalId
|
|
92
|
+
function doneLine(goalId ) {
|
|
93
93
|
return `Every item of ${goalId} is met; ask the user, then: doflow-run goal --action done --goal ${goalId} --channel question`;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
// ── add, item ──────────────────────────────────────────────────────────────────────────────────
|
|
97
97
|
|
|
98
98
|
/** IC-022 `add`: one `goal.added` event with the outcome and the checklist, ids `C1`, `C2`, ... */
|
|
99
|
-
function addGoal({ root, goal, statement, items, channel, now = new Date(), fsImpl = nodeFs }
|
|
99
|
+
function addGoal({ root, goal, statement, items, channel, now = new Date(), fsImpl = nodeFs } ) {
|
|
100
100
|
const id = goalIdOf(goal);
|
|
101
101
|
const outcome = requiredText(statement, '--statement');
|
|
102
102
|
const list = Array.isArray(items) ? items : items === undefined ? [] : [items];
|
|
103
103
|
if (list.length === 0) throw new FollowupUsageError('--item is required for --action add: at least one checklist item (repeat --item for more)');
|
|
104
104
|
if (list.length > ITEMS_MAX) throw new FollowupUsageError(`--item was given ${list.length} times; a goal starts with at most ${ITEMS_MAX} items`);
|
|
105
|
-
const problems
|
|
105
|
+
const problems = [];
|
|
106
106
|
const checklist = list.map((raw, index) => ({ id: `C${index + 1}`, text: oneLine(raw, `--item ${index + 1}`, problems) }));
|
|
107
107
|
if (problems.length) throw new FollowupUsageError(problems.join('; '));
|
|
108
108
|
const by = channelBy(channel);
|
|
109
|
-
if (readFold(root, { fsImpl, now }).goals.some((g
|
|
109
|
+
if (readFold(root, { fsImpl, now }).goals.some((g ) => g.goal === id)) return refusal('add', 'goal-exists', `${id} already exists. Nothing was written.`);
|
|
110
110
|
const out = appendEvents(root, [{ type: 'goal.added', by, data: { goal: id, outcome, items: checklist } }], { now, fsImpl });
|
|
111
111
|
if (!out.ok) return refusalFrom('add', out);
|
|
112
112
|
return {
|
|
113
|
-
ok: true, action: 'add', goal: id, outcome, items: checklist, events: out.written.map((w
|
|
113
|
+
ok: true, action: 'add', goal: id, outcome, items: checklist, events: out.written.map((w ) => w.file),
|
|
114
114
|
next: [`Link a feature that serves it: doflow-run goal --action link --goal ${id} --slug <slug>`, checkLine(id, 'C1')],
|
|
115
115
|
};
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
/** IC-022 `item`: one `goal.item-added` event; the id is the next free `C<n>`. */
|
|
119
|
-
function addItem({ root, goal, text, channel, now = new Date(), fsImpl = nodeFs }
|
|
119
|
+
function addItem({ root, goal, text, channel, now = new Date(), fsImpl = nodeFs } ) {
|
|
120
120
|
const id = goalIdOf(goal);
|
|
121
121
|
const itemText = requiredText(text, '--text');
|
|
122
122
|
const by = channelBy(channel);
|
|
123
123
|
// The id is chosen before the lock, so a writer that took it first makes this call refuse; a retry picks the next one.
|
|
124
124
|
for (let attempt = 0; ; attempt += 1) {
|
|
125
125
|
const fold = readFold(root, { fsImpl, now });
|
|
126
|
-
const found = fold.goals.find((g
|
|
126
|
+
const found = fold.goals.find((g ) => g.goal === id);
|
|
127
127
|
if (!found) return unknownGoal('item', fold, id);
|
|
128
128
|
const item = { id: nextItemId(found), text: itemText };
|
|
129
129
|
const out = appendEvents(root, [{ type: 'goal.item-added', by, data: { goal: id, item } }], { now, fsImpl });
|
|
130
|
-
if (out.ok) return { ok: true, action: 'item', goal: id, item, events: out.written.map((w
|
|
130
|
+
if (out.ok) return { ok: true, action: 'item', goal: id, item, events: out.written.map((w ) => w.file), next: [checkLine(id, item.id)] };
|
|
131
131
|
const raced = out.conflict && out.conflict.code === 'illegal-transition' && /already has item/.test(out.conflict.reason);
|
|
132
132
|
if (!raced || attempt >= 4) return refusalFrom('item', out);
|
|
133
133
|
}
|
|
@@ -136,39 +136,39 @@ function addItem({ root, goal, text, channel, now = new Date(), fsImpl = nodeFs
|
|
|
136
136
|
// ── check, link ────────────────────────────────────────────────────────────────────────────────
|
|
137
137
|
|
|
138
138
|
/** IC-022 `check`: `goal.checked` with `met: true`, or `met: false` under `--unmet`; evidence either way (DEC-015). */
|
|
139
|
-
function checkItem({ root, goal, item, evidence, unmet = false, channel, now = new Date(), fsImpl = nodeFs }
|
|
139
|
+
function checkItem({ root, goal, item, evidence, unmet = false, channel, now = new Date(), fsImpl = nodeFs } ) {
|
|
140
140
|
const id = goalIdOf(goal);
|
|
141
141
|
const itemId = itemIdOf(item);
|
|
142
142
|
const proof = requiredText(evidence, '--evidence');
|
|
143
143
|
const by = channelBy(channel);
|
|
144
144
|
const before = readFold(root, { fsImpl, now });
|
|
145
|
-
const found = before.goals.find((g
|
|
145
|
+
const found = before.goals.find((g ) => g.goal === id);
|
|
146
146
|
if (!found) return unknownGoal('check', before, id);
|
|
147
|
-
if (!found.items.some((i
|
|
148
|
-
return refusal('check', 'unknown-item', `${id} has no item ${itemId}; its items are ${found.items.map((i
|
|
147
|
+
if (!found.items.some((i ) => i.id === itemId)) {
|
|
148
|
+
return refusal('check', 'unknown-item', `${id} has no item ${itemId}; its items are ${found.items.map((i ) => i.id).join(', ')}. Nothing was written.`);
|
|
149
149
|
}
|
|
150
150
|
const met = !unmet;
|
|
151
151
|
const out = appendEvents(root, [{ type: 'goal.checked', by, data: { goal: id, item: itemId, met, evidence: proof } }], { now, fsImpl });
|
|
152
152
|
if (!out.ok) return refusalFrom('check', out);
|
|
153
|
-
const after = readFold(root, { fsImpl, now }).goals.find((g
|
|
153
|
+
const after = readFold(root, { fsImpl, now }).goals.find((g ) => g.goal === id) || found;
|
|
154
154
|
const progress = progressOf(after);
|
|
155
155
|
const proposeDone = after.status === 'open' && progress.total > 0 && progress.met === progress.total;
|
|
156
|
-
const unchecked = after.items.find((i
|
|
156
|
+
const unchecked = after.items.find((i ) => !i.met);
|
|
157
157
|
return {
|
|
158
|
-
ok: true, action: 'check', goal: id, item: itemId, met, evidence: proof, progress, proposeDone, events: out.written.map((w
|
|
158
|
+
ok: true, action: 'check', goal: id, item: itemId, met, evidence: proof, progress, proposeDone, events: out.written.map((w ) => w.file),
|
|
159
159
|
next: proposeDone ? [doneLine(id)] : unchecked && after.status === 'open' ? [checkLine(id, unchecked.id)] : [],
|
|
160
160
|
};
|
|
161
161
|
}
|
|
162
162
|
|
|
163
163
|
/** IC-022 `link`: one `goal.linked` event; a feature serves one goal at most (DEC-009). */
|
|
164
|
-
function linkFeature({ root, goal, slug, replace = false, channel, now = new Date(), fsImpl = nodeFs }
|
|
164
|
+
function linkFeature({ root, goal, slug, replace = false, channel, now = new Date(), fsImpl = nodeFs } ) {
|
|
165
165
|
const id = goalIdOf(goal);
|
|
166
166
|
if (!slug) throw new FollowupUsageError('--slug is required for --action link');
|
|
167
167
|
if (!isSafeSlug(slug)) throw new FollowupUsageError(invalidSlugRefusal(slug).message);
|
|
168
168
|
const by = channelBy(channel);
|
|
169
169
|
const fold = readFold(root, { fsImpl, now });
|
|
170
|
-
if (!fold.goals.some((g
|
|
171
|
-
const feature = fold.features.find((f
|
|
170
|
+
if (!fold.goals.some((g ) => g.goal === id)) return unknownGoal('link', fold, id);
|
|
171
|
+
const feature = fold.features.find((f ) => f.slug === slug);
|
|
172
172
|
if (!feature) return refusal('link', 'untracked-feature', `${slug} is not a tracked feature, so no goal can link it; run doflow-run lifecycle --action init --slug ${slug} first. Nothing was written.`);
|
|
173
173
|
if (feature.goal === id) return { ok: true, action: 'link', goal: id, slug, linked: 'already', replaced: null, events: [], next: [] };
|
|
174
174
|
if (feature.goal && !replace) {
|
|
@@ -176,17 +176,17 @@ function linkFeature({ root, goal, slug, replace = false, channel, now = new Dat
|
|
|
176
176
|
}
|
|
177
177
|
const out = appendEvents(root, [{ type: 'goal.linked', by, data: { goal: id, slug, replace: Boolean(replace) } }], { now, fsImpl });
|
|
178
178
|
if (!out.ok) return refusalFrom('link', out);
|
|
179
|
-
return { ok: true, action: 'link', goal: id, slug, linked: 'new', replaced: feature.goal || null, events: out.written.map((w
|
|
179
|
+
return { ok: true, action: 'link', goal: id, slug, linked: 'new', replaced: feature.goal || null, events: out.written.map((w ) => w.file), next: [] };
|
|
180
180
|
}
|
|
181
181
|
|
|
182
182
|
// ── list ───────────────────────────────────────────────────────────────────────────────────────
|
|
183
183
|
|
|
184
184
|
/** IC-022 progress for one goal: items, `progress`, `proposeDone`, linked features by status and the nudges. */
|
|
185
|
-
function progressView(goal
|
|
185
|
+
function progressView(goal , fold , statuses , conflicts = []) {
|
|
186
186
|
const view = goalView(goal, fold.features, statuses, conflicts);
|
|
187
187
|
return {
|
|
188
188
|
goal: goal.goal, outcome: goal.outcome, status: goal.status,
|
|
189
|
-
items: goal.items.map((i
|
|
189
|
+
items: goal.items.map((i ) => ({ id: i.id, text: i.text, met: i.met, evidence: i.evidence })),
|
|
190
190
|
progress: view.items, proposeDone: view.proposeDone, features: view.features, nudges: view.nudges,
|
|
191
191
|
...(view.conflicts ? { conflicts: view.conflicts } : {}),
|
|
192
192
|
...(goal.status === 'done' ? { reason: goal.reason } : {}),
|
|
@@ -194,25 +194,25 @@ function progressView(goal: any, fold: any, statuses: any, conflicts = []) {
|
|
|
194
194
|
}
|
|
195
195
|
|
|
196
196
|
/** IC-022 `list`: every goal, or the one named, with its progress. Reads git only when a goal has a linked feature. */
|
|
197
|
-
function listGoals({ root, goal, now = new Date(), fsImpl = nodeFs }
|
|
197
|
+
function listGoals({ root, goal, now = new Date(), fsImpl = nodeFs } ) {
|
|
198
198
|
const id = goal === undefined ? null : goalIdOf(goal);
|
|
199
199
|
const fold = readFold(root, { fsImpl, now });
|
|
200
|
-
const shown = id === null ? fold.goals : fold.goals.filter((g
|
|
200
|
+
const shown = id === null ? fold.goals : fold.goals.filter((g ) => g.goal === id);
|
|
201
201
|
if (id !== null && shown.length === 0) return unknownGoal('list', fold, id);
|
|
202
|
-
const linked = new Set(fold.features.filter((f
|
|
202
|
+
const linked = new Set(fold.features.filter((f ) => shown.some((g ) => g.goal === f.goal)).map((f ) => f.slug));
|
|
203
203
|
const statuses = linked.size ? deriveStatuses({ root, fold }) : { statuses: {} };
|
|
204
204
|
const conflictsByGoal = goalConflicts(root, fold, fsImpl);
|
|
205
|
-
const goals = shown.map((g
|
|
205
|
+
const goals = shown.map((g ) => progressView(g, fold, statuses, conflictsByGoal.get(g.goal)));
|
|
206
206
|
const next = [];
|
|
207
|
-
for (const g of goals.filter((x
|
|
207
|
+
for (const g of goals.filter((x ) => x.status === 'open')) {
|
|
208
208
|
if (g.proposeDone) next.push(doneLine(g.goal));
|
|
209
209
|
else {
|
|
210
|
-
const unchecked = g.items.find((i
|
|
210
|
+
const unchecked = g.items.find((i ) => !i.met);
|
|
211
211
|
if (unchecked) next.push(checkLine(g.goal, unchecked.id));
|
|
212
212
|
}
|
|
213
213
|
}
|
|
214
|
-
for (const g of goals.filter((x
|
|
215
|
-
const result
|
|
214
|
+
for (const g of goals.filter((x ) => x.conflicts)) next.push(...goalConflictNext(g.goal, g.conflicts));
|
|
215
|
+
const result = { ok: true, action: 'list', goals, conflicts: fold.conflicts, unreadable: fold.unreadable, unreadableReasons: fold.unreadableReasons, next };
|
|
216
216
|
if (statuses.reason) result.reason = statuses.reason;
|
|
217
217
|
return result;
|
|
218
218
|
}
|
|
@@ -223,7 +223,7 @@ function listGoals({ root, goal, now = new Date(), fsImpl = nodeFs }: any) {
|
|
|
223
223
|
* IC-022 `done`: one `goal.done` event. Only the user closes a goal (DEC-004), so `default` is
|
|
224
224
|
* refused; with unmet items it is refused unless `--reason` says why the user closes it anyway.
|
|
225
225
|
*/
|
|
226
|
-
function doneGoal({ root, goal, reason, channel, now = new Date(), fsImpl = nodeFs }
|
|
226
|
+
function doneGoal({ root, goal, reason, channel, now = new Date(), fsImpl = nodeFs } ) {
|
|
227
227
|
const id = goalIdOf(goal);
|
|
228
228
|
const why = textOf(reason, '--reason');
|
|
229
229
|
const by = channelBy(channel);
|
|
@@ -231,16 +231,16 @@ function doneGoal({ root, goal, reason, channel, now = new Date(), fsImpl = node
|
|
|
231
231
|
return refusal('done', 'not-user', `only the user marks a goal done: pass --channel question, gate or prompt once the user has said so (got ${channel ? `'${channel}'` : 'no channel'}). Nothing was written.`);
|
|
232
232
|
}
|
|
233
233
|
const fold = readFold(root, { fsImpl, now });
|
|
234
|
-
const found = fold.goals.find((g
|
|
234
|
+
const found = fold.goals.find((g ) => g.goal === id);
|
|
235
235
|
if (!found) return unknownGoal('done', fold, id);
|
|
236
236
|
if (found.status === 'done') return refusal('done', 'already-done', `${id} is already done. Nothing was written.`);
|
|
237
|
-
const unmet = found.items.filter((i
|
|
237
|
+
const unmet = found.items.filter((i ) => !i.met).map((i ) => i.id);
|
|
238
238
|
if (unmet.length && !why) {
|
|
239
239
|
return refusal('done', 'items-unmet', `${id} has unmet items (${unmet.join(', ')}); check them with evidence, or pass --reason to say why the user closes it anyway. Nothing was written.`);
|
|
240
240
|
}
|
|
241
241
|
const out = appendEvents(root, [{ type: 'goal.done', by, data: { goal: id, reason: why } }], { now, fsImpl });
|
|
242
242
|
if (!out.ok) return refusalFrom('done', out);
|
|
243
|
-
return { ok: true, action: 'done', goal: id, reason: why, unmet, events: out.written.map((w
|
|
243
|
+
return { ok: true, action: 'done', goal: id, reason: why, unmet, events: out.written.map((w ) => w.file), next: [] };
|
|
244
244
|
}
|
|
245
245
|
|
|
246
246
|
module.exports = {
|
|
@@ -27,11 +27,11 @@ const INTENT_SECTIONS = [
|
|
|
27
27
|
];
|
|
28
28
|
|
|
29
29
|
/** `Cart robustness!` -> `cart-robustness`; empty when the title holds no letter or digit. */
|
|
30
|
-
function kebabTitle(title
|
|
30
|
+
function kebabTitle(title ) {
|
|
31
31
|
return String(title).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, SLUG_MAX).replace(/-+$/, '');
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
function sourceParts(source
|
|
34
|
+
function sourceParts(source ) {
|
|
35
35
|
switch (source.kind) {
|
|
36
36
|
case 'stage': return [source.kind, source.feature, source.stage];
|
|
37
37
|
case 'run': return [source.kind, source.taskClass, source.taskId, source.stage];
|
|
@@ -46,7 +46,7 @@ function sourceParts(source: any): any[] {
|
|
|
46
46
|
* @param {{title:string, by:'user'|'agent', date:string, items:Array<{id:string,statement:string,source:Object}>}} intent
|
|
47
47
|
* @returns {string} the file content
|
|
48
48
|
*/
|
|
49
|
-
function renderIntent({ title, by, date, items }
|
|
49
|
+
function renderIntent({ title, by, date, items } ) {
|
|
50
50
|
const lines = [
|
|
51
51
|
`# Intent: ${title}`,
|
|
52
52
|
'',
|
|
@@ -65,7 +65,7 @@ function renderIntent({ title, by, date, items }: { title: string; by: string; d
|
|
|
65
65
|
* @returns {{ok:true, file:string, path:string}|{ok:false, finding:'intent-exists'|'invalid-title', message:string}}
|
|
66
66
|
* `path` is root-relative with `/` separators.
|
|
67
67
|
*/
|
|
68
|
-
function writeIntent(root
|
|
68
|
+
function writeIntent(root , intent , { fsImpl = nodeFs } = {}) {
|
|
69
69
|
const title = String(intent.title ?? '').trim();
|
|
70
70
|
const name = kebabTitle(title) || intent.fallbackName || '';
|
|
71
71
|
if (!name || title.length > TITLE_MAX || /[\r\n\u2028\u2029\u0085]/.test(title)) {
|
|
@@ -76,7 +76,7 @@ function writeIntent(root: string, intent: any, { fsImpl = nodeFs }: any = {}):
|
|
|
76
76
|
fsImpl.mkdirSync(path.dirname(file), { recursive: true });
|
|
77
77
|
try {
|
|
78
78
|
fsImpl.writeFileSync(file, renderIntent({ ...intent, title }), { encoding: 'utf8', flag: 'wx' });
|
|
79
|
-
} catch (error
|
|
79
|
+
} catch (error ) {
|
|
80
80
|
if (error.code === 'EEXIST') return { ok: false, finding: 'intent-exists', message: `${rel.split(path.sep).join('/')} already exists and was left untouched. Nothing was written.` };
|
|
81
81
|
throw error;
|
|
82
82
|
}
|
|
@@ -11,15 +11,15 @@
|
|
|
11
11
|
|
|
12
12
|
const nodeFs = require('node:fs');
|
|
13
13
|
const path = require('node:path');
|
|
14
|
-
const { isSafeSlug, invalidSlugRefusal } = require('../task-scope.
|
|
15
|
-
const { withDerivedDone } = require('./fold.
|
|
16
|
-
const { appendEvents, readFold, readEvents } = require('./event-store.
|
|
17
|
-
const { deriveStatuses, bucketize, behindNote } = require('./status.
|
|
18
|
-
const { FollowupUsageError, parseIds } = require('./followup.
|
|
14
|
+
const { isSafeSlug, invalidSlugRefusal } = require('../task-scope.js');
|
|
15
|
+
const { withDerivedDone } = require('./fold.js');
|
|
16
|
+
const { appendEvents, readFold, readEvents } = require('./event-store.js');
|
|
17
|
+
const { deriveStatuses, bucketize, behindNote } = require('./status.js');
|
|
18
|
+
const { FollowupUsageError, parseIds } = require('./followup.js');
|
|
19
19
|
|
|
20
20
|
/** The failure store, or null when its module cannot be loaded: the overview then reports no failures. */
|
|
21
21
|
function failureStoreOrNull() {
|
|
22
|
-
try { return require('../failure/store.
|
|
22
|
+
try { return require('../failure/store.js'); } catch { return null; }
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
const DISCOVERY_SHOWN = 15;
|
|
@@ -27,29 +27,29 @@ const MAINTAIN_SHOWN = 50;
|
|
|
27
27
|
/** Slugs shown per finished or awaiting-release bucket: those lists only grow, and the overview runs on every brainstorm. */
|
|
28
28
|
const FEATURES_SHOWN = 10;
|
|
29
29
|
|
|
30
|
-
const refusal = (action
|
|
30
|
+
const refusal = (action , finding , message ) => ({ ok: false, action, finding, message });
|
|
31
31
|
|
|
32
|
-
function statusMap(derived
|
|
33
|
-
return Object.fromEntries(Object.entries(derived.statuses).map(([slug, entry]
|
|
32
|
+
function statusMap(derived ) {
|
|
33
|
+
return Object.fromEntries(Object.entries(derived.statuses).map(([slug, entry] ) => [slug, entry.status]));
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
/** The new and regressed failure entries inside the DoFlow repository, else null. */
|
|
37
|
-
function failureEntries(root
|
|
37
|
+
function failureEntries(root ) {
|
|
38
38
|
const failureStore = failureStoreOrNull();
|
|
39
39
|
if (failureStore === null || !failureStore.isDoflowRepo(root)) return null;
|
|
40
|
-
return failureStore.loadEntries().entries.filter((e
|
|
40
|
+
return failureStore.loadEntries().entries.filter((e ) => failureStore.SHOWN_BY_DEFAULT.has(e.status)).map(failureStore.listedEntry);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
/** Reports first, then the oldest first; the id keeps equal instants in a stable order. */
|
|
44
|
-
function byDiscoveryOrder(a
|
|
44
|
+
function byDiscoveryOrder(a , b ) {
|
|
45
45
|
const report = Number(b.source.kind === 'report') - Number(a.source.kind === 'report');
|
|
46
46
|
if (report !== 0) return report;
|
|
47
47
|
const delta = Date.parse(a.history[0].at) - Date.parse(b.history[0].at);
|
|
48
48
|
return delta !== 0 ? delta : (a.id < b.id ? -1 : 1);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
function shownItem(item
|
|
52
|
-
const shown
|
|
51
|
+
function shownItem(item , pending ) {
|
|
52
|
+
const shown = {
|
|
53
53
|
id: item.id, statement: item.statement, source: item.source, state: item.state,
|
|
54
54
|
promoted: Boolean(item.intent), intent: item.intent, added: item.added,
|
|
55
55
|
};
|
|
@@ -58,7 +58,7 @@ function shownItem(item: any, pending: any) {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
/** A bucket for display: the true count and the most recently tracked slugs, in tracking order. */
|
|
61
|
-
function shownBucket(slugs
|
|
61
|
+
function shownBucket(slugs ) {
|
|
62
62
|
const shown = slugs.slice(-FEATURES_SHOWN);
|
|
63
63
|
return { count: slugs.length, shown, more: slugs.length - shown.length };
|
|
64
64
|
}
|
|
@@ -70,11 +70,11 @@ function shownBucket(slugs: any) {
|
|
|
70
70
|
* whose text the goal now holds (the user re-added it) is no longer reported.
|
|
71
71
|
* @returns {Map<string, Array<Object>>}
|
|
72
72
|
*/
|
|
73
|
-
function goalConflicts(root
|
|
73
|
+
function goalConflicts(root , fold , fsImpl = nodeFs) {
|
|
74
74
|
const byGoal = new Map();
|
|
75
|
-
const raised = fold.conflicts.filter((c
|
|
75
|
+
const raised = fold.conflicts.filter((c ) => typeof c.type === 'string' && c.type.startsWith('goal.'));
|
|
76
76
|
if (raised.length === 0) return byGoal;
|
|
77
|
-
const events = new Map
|
|
77
|
+
const events = new Map (readEvents(root, { fsImpl }).events.map((e ) => [e.id, e]));
|
|
78
78
|
for (const c of raised) {
|
|
79
79
|
const event = events.get(c.event);
|
|
80
80
|
const goal = event && event.data && event.data.goal;
|
|
@@ -82,8 +82,8 @@ function goalConflicts(root: any, fold: any, fsImpl = nodeFs) {
|
|
|
82
82
|
const entry = { ...c };
|
|
83
83
|
if (c.type === 'goal.item-added' && event.data.item) {
|
|
84
84
|
// Event files are never removed, so the dropped item is resolved once the goal holds an item with that text.
|
|
85
|
-
const held = fold.goals.find((g
|
|
86
|
-
if (held && held.items.some((i
|
|
85
|
+
const held = fold.goals.find((g ) => g.goal === goal);
|
|
86
|
+
if (held && held.items.some((i ) => i.text === String(event.data.item.text ?? ''))) continue;
|
|
87
87
|
Object.assign(entry, { item: event.data.item.id, text: event.data.item.text });
|
|
88
88
|
}
|
|
89
89
|
if (!byGoal.has(goal)) byGoal.set(goal, []);
|
|
@@ -93,29 +93,29 @@ function goalConflicts(root: any, fold: any, fsImpl = nodeFs) {
|
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
/** One `next` line per kind of goal conflict, telling the user how to resolve it. */
|
|
96
|
-
function goalConflictNext(goalId
|
|
96
|
+
function goalConflictNext(goalId , conflicts ) {
|
|
97
97
|
const next = [];
|
|
98
|
-
if (conflicts.some((c
|
|
98
|
+
if (conflicts.some((c ) => c.type === 'goal.item-added')) {
|
|
99
99
|
next.push(`An item added to ${goalId} from another clone was dropped because its id was already taken; re-add it under a new text: doflow-run goal --action item --goal ${goalId} --text "<the item>"`);
|
|
100
100
|
}
|
|
101
|
-
if (conflicts.some((c
|
|
101
|
+
if (conflicts.some((c ) => c.type === 'goal.added')) {
|
|
102
102
|
next.push(`Another clone added goal ${goalId} first and its record won; re-add yours under a different goal id: doflow-run goal --action add --goal <new-id> --statement "<outcome>"`);
|
|
103
103
|
}
|
|
104
|
-
if (conflicts.some((c
|
|
104
|
+
if (conflicts.some((c ) => c.type !== 'goal.item-added' && c.type !== 'goal.added')) {
|
|
105
105
|
next.push(`An event for ${goalId} from another clone was dropped (see conflicts); redo it with doflow-run goal --action list as the current state`);
|
|
106
106
|
}
|
|
107
107
|
return next;
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
/** Progress, linked features by status and the nudges of one open goal (IC-022). */
|
|
111
|
-
function goalView(goal
|
|
112
|
-
const linked = features.filter((f
|
|
113
|
-
const buckets = bucketize(Object.fromEntries(linked.map((slug
|
|
114
|
-
const met = goal.items.filter((i
|
|
111
|
+
function goalView(goal , features , statuses , conflicts = []) {
|
|
112
|
+
const linked = features.filter((f ) => f.goal === goal.goal).map((f ) => f.slug);
|
|
113
|
+
const buckets = bucketize(Object.fromEntries(linked.map((slug ) => [slug, statuses.statuses[slug] || { status: 'unknown' }])), linked);
|
|
114
|
+
const met = goal.items.filter((i ) => i.met).length;
|
|
115
115
|
const nudges = [];
|
|
116
116
|
if (buckets.finished.length) {
|
|
117
117
|
const who = buckets.finished.length === 1 ? `linked feature ${buckets.finished[0]} is finished` : `linked features ${buckets.finished.join(', ')} are finished`;
|
|
118
|
-
for (const item of goal.items.filter((i
|
|
118
|
+
for (const item of goal.items.filter((i ) => !i.met)) nudges.push(`${item.id} is unchecked and ${who}`);
|
|
119
119
|
}
|
|
120
120
|
return {
|
|
121
121
|
goal: goal.goal, outcome: goal.outcome,
|
|
@@ -127,7 +127,7 @@ function goalView(goal: any, features: any, statuses: any, conflicts: any = [])
|
|
|
127
127
|
};
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
-
function nextLines({ shown, open, items, intents, goals, maintain, pendingItems, failures }
|
|
130
|
+
function nextLines({ shown, open, items, intents, goals, maintain, pendingItems, failures } ) {
|
|
131
131
|
const next = [];
|
|
132
132
|
if (shown < open) next.push('List the rest: doflow-run followup --action list');
|
|
133
133
|
if (items.length) next.push(`Take items into the new feature when its folder exists: doflow-run lifecycle --action init --slug <slug> --take ${items[0].id}`);
|
|
@@ -135,10 +135,10 @@ function nextLines({ shown, open, items, intents, goals, maintain, pendingItems,
|
|
|
135
135
|
next.push(`Start from a promoted intent: /do-brainstorm --intent ${intents[0].path}, then doflow-run lifecycle --action init --slug <slug> --intent ${intents[0].path}`);
|
|
136
136
|
}
|
|
137
137
|
if (goals.length) next.push(`Link the new feature to a goal: add --goal ${goals[0].goal} to the init line`);
|
|
138
|
-
for (const g of goals.filter((x
|
|
138
|
+
for (const g of goals.filter((x ) => x.conflicts)) next.push(...goalConflictNext(g.goal, g.conflicts));
|
|
139
139
|
if (maintain && pendingItems.length) {
|
|
140
140
|
const id = pendingItems[0].id;
|
|
141
|
-
const unpromoted = pendingItems.find((i
|
|
141
|
+
const unpromoted = pendingItems.find((i ) => !i.promoted);
|
|
142
142
|
next.push(
|
|
143
143
|
`Keep open: doflow-run followup --action settle --ids ${id} --as kept --reason "<why it stays>" --channel question`,
|
|
144
144
|
`Dismiss: doflow-run followup --action settle --ids ${id} --as dismissed --reason "<why>" --channel question`,
|
|
@@ -151,7 +151,7 @@ function nextLines({ shown, open, items, intents, goals, maintain, pendingItems,
|
|
|
151
151
|
next.push('Settle a failure entry: doflow-run failure --action settle --fp <fp> --as noise|fixed|imported --reason "<why>"');
|
|
152
152
|
}
|
|
153
153
|
if (maintain) {
|
|
154
|
-
for (const goal of goals.filter((g
|
|
154
|
+
for (const goal of goals.filter((g ) => g.proposeDone)) {
|
|
155
155
|
next.push(`Every item of ${goal.goal} is met; ask the user, then: doflow-run goal --action done --goal ${goal.goal} --channel question`);
|
|
156
156
|
}
|
|
157
157
|
}
|
|
@@ -165,7 +165,7 @@ function nextLines({ shown, open, items, intents, goals, maintain, pendingItems,
|
|
|
165
165
|
* @param {boolean} [options.maintain] the `/do maintain` view: up to 50 items and `pending`
|
|
166
166
|
* @param {string} [options.since] ISO time; with `maintain`, an open item with no settlement at or after it is pending
|
|
167
167
|
*/
|
|
168
|
-
function buildOverview({ root, maintain = false, since, now = new Date(), fsImpl = nodeFs }
|
|
168
|
+
function buildOverview({ root, maintain = false, since, now = new Date(), fsImpl = nodeFs } ) {
|
|
169
169
|
let sinceMs = null;
|
|
170
170
|
if (since !== undefined && since !== null) {
|
|
171
171
|
sinceMs = Date.parse(since);
|
|
@@ -174,25 +174,25 @@ function buildOverview({ root, maintain = false, since, now = new Date(), fsImpl
|
|
|
174
174
|
const fold = readFold(root, { fsImpl, now });
|
|
175
175
|
const derived = deriveStatuses({ root, fold });
|
|
176
176
|
const followups = withDerivedDone(fold.followups, statusMap(derived));
|
|
177
|
-
const open = followups.filter((item
|
|
177
|
+
const open = followups.filter((item ) => item.state === 'open');
|
|
178
178
|
const limit = maintain ? MAINTAIN_SHOWN : DISCOVERY_SHOWN;
|
|
179
179
|
// A promotion settles an item for the maintain loop as much as a settlement does: it stays open with an intent.
|
|
180
|
-
const settledSince = (item
|
|
181
|
-
const pendingOf = (item
|
|
180
|
+
const settledSince = (item ) => item.history.some((h ) => (h.type === 'followup.settled' || h.type === 'followup.promoted') && (sinceMs === null || Date.parse(h.at) >= sinceMs));
|
|
181
|
+
const pendingOf = (item ) => !settledSince(item);
|
|
182
182
|
// Maintain shows pending items first (IC-007), so a loop over more than MAINTAIN_SHOWN items reaches the rest.
|
|
183
183
|
const ordered = [...open].sort((a, b) => (maintain ? Number(pendingOf(b)) - Number(pendingOf(a)) : 0) || byDiscoveryOrder(a, b));
|
|
184
184
|
const items = ordered.slice(0, limit).map((item) => shownItem(item, maintain ? pendingOf(item) : undefined));
|
|
185
185
|
|
|
186
186
|
const intentGroups = new Map();
|
|
187
|
-
for (const item of open.filter((i
|
|
187
|
+
for (const item of open.filter((i ) => i.intent)) {
|
|
188
188
|
if (!intentGroups.has(item.intent)) intentGroups.set(item.intent, []);
|
|
189
189
|
intentGroups.get(item.intent).push(item.id);
|
|
190
190
|
}
|
|
191
191
|
const intents = [...intentGroups].map(([intentPath, ids]) => ({ path: intentPath, items: ids }));
|
|
192
192
|
const conflictsByGoal = goalConflicts(root, fold, fsImpl);
|
|
193
|
-
const goals = fold.goals.filter((g
|
|
193
|
+
const goals = fold.goals.filter((g ) => g.status === 'open').map((g ) => goalView(g, fold.features, derived, conflictsByGoal.get(g.goal)));
|
|
194
194
|
|
|
195
|
-
const result
|
|
195
|
+
const result = {
|
|
196
196
|
ok: true,
|
|
197
197
|
mode: maintain ? 'maintain' : 'discovery',
|
|
198
198
|
releaseMode: derived.releaseMode,
|
|
@@ -228,7 +228,7 @@ function buildOverview({ root, maintain = false, since, now = new Date(), fsImpl
|
|
|
228
228
|
* The feature folder is looked for under the IC-001 root itself, never in the current worktree, so a
|
|
229
229
|
* folder that exists only in a linked worktree is refused.
|
|
230
230
|
*/
|
|
231
|
-
function initFeature({ root, slug, take, intent, goal, now = new Date(), fsImpl = nodeFs }
|
|
231
|
+
function initFeature({ root, slug, take, intent, goal, now = new Date(), fsImpl = nodeFs } ) {
|
|
232
232
|
if (!slug) throw new FollowupUsageError('--slug is required for --action init');
|
|
233
233
|
const bad = invalidSlugRefusal(slug);
|
|
234
234
|
if (bad) throw new FollowupUsageError(bad.message);
|
|
@@ -240,7 +240,7 @@ function initFeature({ root, slug, take, intent, goal, now = new Date(), fsImpl
|
|
|
240
240
|
}
|
|
241
241
|
const wanted = take === undefined || take === null ? [] : parseIds(take, '--take');
|
|
242
242
|
const fold = readFold(root, { fsImpl, now });
|
|
243
|
-
const tracked = fold.features.find((f
|
|
243
|
+
const tracked = fold.features.find((f ) => f.slug === slug);
|
|
244
244
|
const ids = [...wanted];
|
|
245
245
|
if (intent) {
|
|
246
246
|
for (const item of fold.followups) if (item.state === 'open' && item.intent === intent && !ids.includes(item.id)) ids.push(item.id);
|
|
@@ -262,7 +262,7 @@ function initFeature({ root, slug, take, intent, goal, now = new Date(), fsImpl
|
|
|
262
262
|
const code = out.conflict && ['unknown-id', 'untracked-feature', 'goal-already-linked', 'unknown-goal'].includes(out.conflict.code) ? out.conflict.code : out.finding;
|
|
263
263
|
return refusal('init', code, out.message);
|
|
264
264
|
}
|
|
265
|
-
events = out.written.map((w
|
|
265
|
+
events = out.written.map((w ) => w.file);
|
|
266
266
|
}
|
|
267
267
|
return { ok: true, action: 'init', slug, tracked: tracked ? 'already' : 'new', taken: ids, goal: goal || (tracked && tracked.goal) || null, events, next };
|
|
268
268
|
}
|
|
@@ -270,19 +270,19 @@ function initFeature({ root, slug, take, intent, goal, now = new Date(), fsImpl
|
|
|
270
270
|
// ── status ─────────────────────────────────────────────────────────────────────────────────────
|
|
271
271
|
|
|
272
272
|
/** IC-021 `status`: the derived status of one tracked feature. */
|
|
273
|
-
function featureStatus({ root, slug, now = new Date(), fsImpl = nodeFs }
|
|
273
|
+
function featureStatus({ root, slug, now = new Date(), fsImpl = nodeFs } ) {
|
|
274
274
|
if (!slug) throw new FollowupUsageError('--slug is required for --action status');
|
|
275
275
|
if (!isSafeSlug(slug)) throw new FollowupUsageError(invalidSlugRefusal(slug).message);
|
|
276
276
|
const fold = readFold(root, { fsImpl, now });
|
|
277
|
-
if (!fold.features.some((f
|
|
277
|
+
if (!fold.features.some((f ) => f.slug === slug)) {
|
|
278
278
|
return refusal('status', 'untracked-feature', `${slug} is not a tracked feature, so no status is derived for it; run doflow-run lifecycle --action init --slug ${slug} first.`);
|
|
279
279
|
}
|
|
280
280
|
const derived = deriveStatuses({ root, fold });
|
|
281
281
|
const entry = derived.statuses[slug];
|
|
282
|
-
const result
|
|
282
|
+
const result = {
|
|
283
283
|
ok: true, action: 'status', slug, status: entry.status, integrationRef: derived.integrationRef,
|
|
284
284
|
evidence: entry.evidence, release: entry.release,
|
|
285
|
-
takenItems: fold.followups.filter((item
|
|
285
|
+
takenItems: fold.followups.filter((item ) => item.takenBy === slug && item.state === 'taken').map((item ) => item.id),
|
|
286
286
|
};
|
|
287
287
|
if (derived.reason) result.reason = derived.reason;
|
|
288
288
|
if (derived.integrationBehind > 0) Object.assign(result, { integrationBehind: derived.integrationBehind, note: behindNote(derived.integrationRef, derived.integrationBehind) });
|