@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
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
const fs = require('node:fs');
|
|
6
6
|
const path = require('node:path');
|
|
7
|
-
const
|
|
7
|
+
const featureResolve = require('./feature-resolve.js');
|
|
8
8
|
|
|
9
9
|
// Which feature a task record belongs to (feature 045, IC-001/IC-002).
|
|
10
10
|
//
|
|
@@ -19,7 +19,7 @@ const SAFE_SLUG = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
|
19
19
|
const SLUG_RULE = 'use letters, digits, dot, underscore or dash, start with a letter or digit, and no ".."';
|
|
20
20
|
|
|
21
21
|
/** The one shape check for a feature slug (IC-009); do-paths.sh applies the same rule in bash. */
|
|
22
|
-
function isSafeSlug(slug
|
|
22
|
+
function isSafeSlug(slug ) {
|
|
23
23
|
return typeof slug === 'string' && SAFE_SLUG.test(slug) && !slug.includes('..');
|
|
24
24
|
}
|
|
25
25
|
|
|
@@ -29,31 +29,56 @@ function isSafeSlug(slug: any): boolean {
|
|
|
29
29
|
* @param {*} slug
|
|
30
30
|
* @returns {{error: 'invalid-slug', message: string, hint: string}|null}
|
|
31
31
|
*/
|
|
32
|
-
function invalidSlugRefusal(slug
|
|
32
|
+
function invalidSlugRefusal(slug ) {
|
|
33
33
|
if (typeof slug !== 'string' || slug === '' || isSafeSlug(slug)) return null;
|
|
34
34
|
return { error: 'invalid-slug', message: `slug "${slug}" is not a valid feature slug: ${SLUG_RULE}`, hint: SLUG_RULE };
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
/** (projectRoot, slug) -> { slug, hasRegister } | { reason }. One resolver spawn per key per process. */
|
|
38
|
-
const featureCache = new Map
|
|
38
|
+
const featureCache = new Map ();
|
|
39
|
+
|
|
40
|
+
/** The resolver's raw answer, including thrown failures, shared by every consumer in this process. */
|
|
41
|
+
const resolvedFeatureCache = new Map ();
|
|
39
42
|
|
|
40
43
|
/** A slug the CLI named explicitly (`--slug`), applied to every store call in this process that was
|
|
41
44
|
* not given its own. The dispatcher sets it once so the verbs whose handlers build their ledger
|
|
42
45
|
* internally (readiness, evidence) route the same way as the ones that take `slug` directly. */
|
|
43
|
-
let defaultSlug
|
|
46
|
+
let defaultSlug = null;
|
|
44
47
|
|
|
45
|
-
function setDefaultSlug(slug
|
|
48
|
+
function setDefaultSlug(slug ) {
|
|
46
49
|
defaultSlug = typeof slug === 'string' && slug !== '' ? slug : null;
|
|
47
50
|
}
|
|
48
51
|
|
|
49
|
-
function clearTaskScopeCache()
|
|
52
|
+
function clearTaskScopeCache() {
|
|
50
53
|
featureCache.clear();
|
|
54
|
+
resolvedFeatureCache.clear();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Resolve once per process per (root, slug). The resolver spawns do-paths.sh, and every store and
|
|
59
|
+
* the decisions lookup must see the same answer.
|
|
60
|
+
*/
|
|
61
|
+
function resolveFeature({ projectRoot, slug = null } ) {
|
|
62
|
+
const key = `${path.resolve(projectRoot)}\0${slug || ''}`;
|
|
63
|
+
if (resolvedFeatureCache.has(key)) {
|
|
64
|
+
const cached = resolvedFeatureCache.get(key) ;
|
|
65
|
+
if ('error' in cached) throw cached.error;
|
|
66
|
+
return cached.value;
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
const value = featureResolve.resolveActiveFeature({ projectRoot, slug: slug || null });
|
|
70
|
+
resolvedFeatureCache.set(key, { value });
|
|
71
|
+
return value;
|
|
72
|
+
} catch (error) {
|
|
73
|
+
resolvedFeatureCache.set(key, { error });
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
51
76
|
}
|
|
52
77
|
|
|
53
78
|
/** The resolver reports the git top level, which is the project root or one of its ancestors, and
|
|
54
79
|
* the feature folders live directly under it — so no `agent-docs/doflow` on the way up means there
|
|
55
80
|
* is nothing to resolve and the spawn is skipped. */
|
|
56
|
-
function hasFeatureFolderAbove(projectRoot
|
|
81
|
+
function hasFeatureFolderAbove(projectRoot ) {
|
|
57
82
|
let dir = path.resolve(projectRoot);
|
|
58
83
|
for (;;) {
|
|
59
84
|
try {
|
|
@@ -65,13 +90,13 @@ function hasFeatureFolderAbove(projectRoot: string): boolean {
|
|
|
65
90
|
}
|
|
66
91
|
}
|
|
67
92
|
|
|
68
|
-
function featureFor(projectRoot
|
|
93
|
+
function featureFor(projectRoot , slug ) {
|
|
69
94
|
const key = `${path.resolve(projectRoot)}\0${slug || ''}`;
|
|
70
95
|
if (featureCache.has(key)) return featureCache.get(key);
|
|
71
|
-
let value
|
|
96
|
+
let value = { reason: 'no-feature' };
|
|
72
97
|
try {
|
|
73
98
|
if (hasFeatureFolderAbove(projectRoot)) {
|
|
74
|
-
const found =
|
|
99
|
+
const found = resolveFeature({ projectRoot, slug: slug || null });
|
|
75
100
|
if (!found.error && found.paths && isSafeSlug(found.paths.feature_slug)) {
|
|
76
101
|
value = { slug: found.paths.feature_slug, hasRegister: found.paths.has_decisions === true, exists: fs.existsSync(found.featureDir) };
|
|
77
102
|
}
|
|
@@ -90,7 +115,7 @@ function featureFor(projectRoot: string, slug: string | null | undefined): any {
|
|
|
90
115
|
* `reason` is `namespaced` when a namespace applies, else `no-feature`, `no-register` or
|
|
91
116
|
* `feature-level-id`.
|
|
92
117
|
*/
|
|
93
|
-
function resolveTaskScope({ projectRoot, taskId, slug = null }
|
|
118
|
+
function resolveTaskScope({ projectRoot, taskId, slug = null } ) {
|
|
94
119
|
const feature = featureFor(projectRoot, slug || defaultSlug);
|
|
95
120
|
if (feature.reason) return { namespace: null, slug: null, reason: feature.reason };
|
|
96
121
|
if (!feature.hasRegister) return { namespace: null, slug: feature.slug, reason: 'no-register' };
|
|
@@ -107,7 +132,7 @@ function resolveTaskScope({ projectRoot, taskId, slug = null }: { projectRoot: s
|
|
|
107
132
|
* @param {string} options.slug
|
|
108
133
|
* @returns {boolean}
|
|
109
134
|
*/
|
|
110
|
-
function slugNamesNoFeature({ projectRoot, slug }
|
|
135
|
+
function slugNamesNoFeature({ projectRoot, slug } ) {
|
|
111
136
|
const feature = featureFor(projectRoot, slug);
|
|
112
137
|
return Boolean(feature.reason) || feature.exists === false;
|
|
113
138
|
}
|
|
@@ -121,10 +146,10 @@ function slugNamesNoFeature({ projectRoot, slug }: { projectRoot: string; slug:
|
|
|
121
146
|
* @param {string|null} [options.slug]
|
|
122
147
|
* @returns {string}
|
|
123
148
|
*/
|
|
124
|
-
function taskStoreDir({ projectRoot, store, taskId, slug = null }
|
|
149
|
+
function taskStoreDir({ projectRoot, store, taskId, slug = null } ) {
|
|
125
150
|
const base = path.join(projectRoot, '.doflow', 'state', store);
|
|
126
151
|
const { namespace } = resolveTaskScope({ projectRoot, taskId, slug });
|
|
127
152
|
return namespace ? path.join(base, namespace) : base;
|
|
128
153
|
}
|
|
129
154
|
|
|
130
|
-
module.exports = { resolveTaskScope, taskStoreDir, setDefaultSlug, clearTaskScopeCache, isSafeSlug, invalidSlugRefusal, slugNamesNoFeature };
|
|
155
|
+
module.exports = { resolveFeature, resolveTaskScope, taskStoreDir, setDefaultSlug, clearTaskScopeCache, isSafeSlug, invalidSlugRefusal, slugNamesNoFeature };
|
|
@@ -28,8 +28,8 @@ const { isDeepStrictEqual } = require('node:util');
|
|
|
28
28
|
|
|
29
29
|
/** Three-way merge against the reader's snapshot. Unchanged local records never overwrite a
|
|
30
30
|
* newer disk version; concurrent edits to the same record must be retried after an explicit read. */
|
|
31
|
-
function mergeRecords(local
|
|
32
|
-
const merged = new Map(disk.map((item
|
|
31
|
+
function mergeRecords(local , disk , baseline ) {
|
|
32
|
+
const merged = new Map(disk.map((item ) => [item.id, item]));
|
|
33
33
|
for (const item of local) {
|
|
34
34
|
const previous = baseline.get(item.id);
|
|
35
35
|
const current = merged.get(item.id);
|
|
@@ -48,7 +48,7 @@ const LOCK_RETRIES = 250;
|
|
|
48
48
|
const LOCK_WAIT_MS = 20;
|
|
49
49
|
|
|
50
50
|
/** Synchronous sleep without spinning: Atomics.wait on a throwaway buffer. */
|
|
51
|
-
function sleep(ms
|
|
51
|
+
function sleep(ms ) {
|
|
52
52
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
53
53
|
}
|
|
54
54
|
|
|
@@ -59,28 +59,28 @@ function sleep(ms: number): void {
|
|
|
59
59
|
* @param {string} file
|
|
60
60
|
* @returns {(() => void)|null}
|
|
61
61
|
*/
|
|
62
|
-
function tryAcquireLock(fsImpl
|
|
62
|
+
function tryAcquireLock(fsImpl , file ) {
|
|
63
63
|
const lockDir = `${file}.lock`;
|
|
64
64
|
const release = () => { try { fsImpl.rmdirSync(lockDir); } catch { /* released is released */ } };
|
|
65
65
|
try {
|
|
66
66
|
fsImpl.mkdirSync(lockDir);
|
|
67
67
|
return release;
|
|
68
|
-
} catch (error
|
|
68
|
+
} catch (error ) {
|
|
69
69
|
if (error.code !== 'EEXIST') throw error;
|
|
70
70
|
}
|
|
71
|
-
let lock
|
|
72
|
-
try { lock = fsImpl.statSync(lockDir); } catch (error
|
|
71
|
+
let lock ;
|
|
72
|
+
try { lock = fsImpl.statSync(lockDir); } catch (error ) {
|
|
73
73
|
if (error.code !== 'ENOENT') return null;
|
|
74
|
-
try { fsImpl.mkdirSync(lockDir); return release; } catch (retryError
|
|
74
|
+
try { fsImpl.mkdirSync(lockDir); return release; } catch (retryError ) {
|
|
75
75
|
if (retryError.code === 'EEXIST') return null;
|
|
76
76
|
throw retryError;
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
if (Date.now() - lock.mtimeMs <= LOCK_STALE_MS) return null;
|
|
80
|
-
try { fsImpl.rmdirSync(lockDir); } catch (error
|
|
80
|
+
try { fsImpl.rmdirSync(lockDir); } catch (error ) {
|
|
81
81
|
if (error.code !== 'ENOENT') return null;
|
|
82
82
|
}
|
|
83
|
-
try { fsImpl.mkdirSync(lockDir); return release; } catch (error
|
|
83
|
+
try { fsImpl.mkdirSync(lockDir); return release; } catch (error ) {
|
|
84
84
|
if (error.code === 'EEXIST') return null;
|
|
85
85
|
throw error;
|
|
86
86
|
}
|
|
@@ -93,7 +93,7 @@ function tryAcquireLock(fsImpl: any, file: string): (() => void) | null {
|
|
|
93
93
|
* @param {string} file
|
|
94
94
|
* @returns {() => void}
|
|
95
95
|
*/
|
|
96
|
-
function acquireLock(fsImpl
|
|
96
|
+
function acquireLock(fsImpl , file ) {
|
|
97
97
|
const lockDir = `${file}.lock`;
|
|
98
98
|
// The stale rule below is wall-clock, so the wait must be too: on a host where each attempt costs
|
|
99
99
|
// real time, counting attempts alone would outlive a live holder's staleness window and then
|
|
@@ -117,25 +117,66 @@ function acquireLock(fsImpl: any, file: string): () => void {
|
|
|
117
117
|
+ 'If no other session is running, the lock is leftover from a crash younger than its stale timeout; retry shortly.');
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
/**
|
|
121
|
+
* Takes the cross-process lock for one state file with a bounded wait.
|
|
122
|
+
* Makes 1 try plus `retries` retries (default 3) separated by `waitMs` (default 5).
|
|
123
|
+
* Throws an Error('store-locked') after one try plus retries.
|
|
124
|
+
* @param {Object} fsImpl
|
|
125
|
+
* @param {string} file
|
|
126
|
+
* @param {Object} [options]
|
|
127
|
+
* @param {number} [options.retries=3]
|
|
128
|
+
* @param {number} [options.waitMs=5]
|
|
129
|
+
* @returns {() => void} release function
|
|
130
|
+
*/
|
|
131
|
+
function boundedAcquire(
|
|
132
|
+
fsImpl ,
|
|
133
|
+
file ,
|
|
134
|
+
options
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
= {}
|
|
140
|
+
) {
|
|
141
|
+
const retries = options?.retries ?? 3;
|
|
142
|
+
const waitMs = options?.waitMs ?? 5;
|
|
143
|
+
const sleepFn = options?.sleepFn ?? sleep;
|
|
144
|
+
const tryAcquire = options?.tryAcquireLockFn ?? tryAcquireLock;
|
|
145
|
+
for (let attempt = 0; attempt <= retries; attempt += 1) {
|
|
146
|
+
const release = tryAcquire(fsImpl, file);
|
|
147
|
+
if (release) return release;
|
|
148
|
+
if (attempt < retries && waitMs > 0) {
|
|
149
|
+
sleepFn(waitMs);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const error = new Error('store-locked');
|
|
153
|
+
(error ).code = 'store-locked';
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
156
|
+
|
|
120
157
|
/**
|
|
121
158
|
* Reads and validates one task-state file. Returns null when the file does not exist.
|
|
122
159
|
* @param {Object} fsImpl
|
|
123
160
|
* @param {string} file
|
|
124
161
|
* @returns {Object|null} the parsed payload
|
|
125
162
|
*/
|
|
126
|
-
function readTaskState(fsImpl
|
|
163
|
+
function readTaskState(fsImpl , file ) {
|
|
127
164
|
if (!fsImpl.existsSync(file)) return null;
|
|
128
|
-
let data
|
|
165
|
+
let data ;
|
|
129
166
|
try {
|
|
130
167
|
data = JSON.parse(fsImpl.readFileSync(file, 'utf8'));
|
|
131
|
-
} catch (error
|
|
132
|
-
|
|
168
|
+
} catch (error ) {
|
|
169
|
+
const failure = new Error(`Failed to load state file '${file}': ${error.message}. The write path is atomic `
|
|
133
170
|
+ '(tmp + rename), so a torn file means outside interference; recover it from version control '
|
|
134
171
|
+ 'or move it aside and re-record the task\'s items.');
|
|
172
|
+
failure.code = 'store-unreadable';
|
|
173
|
+
throw failure;
|
|
135
174
|
}
|
|
136
175
|
if (data && typeof data === 'object' && data.version !== undefined && data.version !== SCHEMA_VERSION) {
|
|
137
|
-
|
|
176
|
+
const failure = new Error(`State file '${file}' has schema version ${data.version}; this runtime reads version ${SCHEMA_VERSION}. `
|
|
138
177
|
+ 'Refusing to guess at its shape — upgrade the runtime or migrate the file.');
|
|
178
|
+
failure.code = 'store-unreadable';
|
|
179
|
+
throw failure;
|
|
139
180
|
}
|
|
140
181
|
return data;
|
|
141
182
|
}
|
|
@@ -151,16 +192,32 @@ function readTaskState(fsImpl: any, file: string): any {
|
|
|
151
192
|
* return the complete new payload (without `version`/`revision`, which are stamped here)
|
|
152
193
|
* @param {() => void} [options.prepare] called INSIDE the lock before the file is read, for a store
|
|
153
194
|
* that must inspect or set aside what is there under the same exclusion as the write
|
|
154
|
-
* @
|
|
195
|
+
* @param {(fsImpl: Object, file: string) => () => void} [options.acquire] optional lock acquirer
|
|
196
|
+
* defaulting to acquireLock
|
|
197
|
+
* @returns {string|null} the file path written, or null when build declines a write
|
|
155
198
|
*/
|
|
156
|
-
function updateTaskState({
|
|
199
|
+
function updateTaskState({
|
|
200
|
+
fsImpl,
|
|
201
|
+
file,
|
|
202
|
+
build,
|
|
203
|
+
prepare,
|
|
204
|
+
acquire = acquireLock,
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
) {
|
|
157
212
|
fsImpl.mkdirSync(path.dirname(file), { recursive: true });
|
|
158
|
-
const release =
|
|
213
|
+
const release = acquire(fsImpl, file);
|
|
159
214
|
try {
|
|
160
215
|
if (prepare) prepare();
|
|
161
216
|
const disk = readTaskState(fsImpl, file);
|
|
217
|
+
const built = build(disk);
|
|
218
|
+
if (built === null) return null;
|
|
162
219
|
const payload = {
|
|
163
|
-
...
|
|
220
|
+
...built,
|
|
164
221
|
version: SCHEMA_VERSION,
|
|
165
222
|
revision: (disk && Number.isInteger(disk.revision) ? disk.revision : 0) + 1,
|
|
166
223
|
};
|
|
@@ -173,4 +230,12 @@ function updateTaskState({ fsImpl, file, build, prepare }: { fsImpl: any; file:
|
|
|
173
230
|
}
|
|
174
231
|
}
|
|
175
232
|
|
|
176
|
-
module.exports = {
|
|
233
|
+
module.exports = {
|
|
234
|
+
acquireLock,
|
|
235
|
+
tryAcquireLock,
|
|
236
|
+
boundedAcquire,
|
|
237
|
+
updateTaskState,
|
|
238
|
+
readTaskState,
|
|
239
|
+
mergeRecords,
|
|
240
|
+
SCHEMA_VERSION,
|
|
241
|
+
};
|
|
@@ -26,16 +26,16 @@
|
|
|
26
26
|
|
|
27
27
|
const fs = require('node:fs');
|
|
28
28
|
const path = require('node:path');
|
|
29
|
-
const { formatMs, finish } = require('./render.
|
|
29
|
+
const { formatMs, finish } = require('./render.js');
|
|
30
30
|
|
|
31
31
|
const ORCHESTRATION_DIRNAME = 'orchestration';
|
|
32
32
|
|
|
33
33
|
/** Mirrors resolveRunsLocation's shape, for the sibling state directory this reads. */
|
|
34
|
-
function orchestrationDir(root
|
|
34
|
+
function orchestrationDir(root ) {
|
|
35
35
|
return path.join(root, '.doflow', 'state', ORCHESTRATION_DIRNAME);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
function parseAt(value
|
|
38
|
+
function parseAt(value ) {
|
|
39
39
|
const ms = Date.parse(value);
|
|
40
40
|
return Number.isNaN(ms) ? null : ms;
|
|
41
41
|
}
|
|
@@ -43,10 +43,10 @@ function parseAt(value: any): number | null {
|
|
|
43
43
|
/** Every record in a directory, plus the names of the ones that did not parse. A file that cannot be
|
|
44
44
|
* read is counted and named rather than skipped: a report that quietly drops what it could not read
|
|
45
45
|
* overstates its own coverage, which is the defect `malformedLines` already guards against for runs. */
|
|
46
|
-
function readRecords(dir
|
|
46
|
+
function readRecords(dir , fsImpl = fs) {
|
|
47
47
|
if (!fsImpl.existsSync(dir)) return { records: [], unreadable: [], exists: false };
|
|
48
|
-
const records
|
|
49
|
-
const unreadable
|
|
48
|
+
const records = [];
|
|
49
|
+
const unreadable = [];
|
|
50
50
|
for (const name of fsImpl.readdirSync(dir).sort()) {
|
|
51
51
|
if (!name.endsWith('.json')) continue;
|
|
52
52
|
try {
|
|
@@ -56,7 +56,7 @@ function readRecords(dir: string, fsImpl: any = fs): any {
|
|
|
56
56
|
continue;
|
|
57
57
|
}
|
|
58
58
|
records.push(parsed);
|
|
59
|
-
} catch (err
|
|
59
|
+
} catch (err ) {
|
|
60
60
|
unreadable.push({ file: name, reason: err.message });
|
|
61
61
|
}
|
|
62
62
|
}
|
|
@@ -65,11 +65,11 @@ function readRecords(dir: string, fsImpl: any = fs): any {
|
|
|
65
65
|
|
|
66
66
|
/** A stage row is "not work done" when the orchestrator backfilled it or imported it rather than
|
|
67
67
|
* executing it. Its duration measures when the record was written, so it is listed but never averaged. */
|
|
68
|
-
function isSynthetic(entry
|
|
68
|
+
function isSynthetic(entry ) {
|
|
69
69
|
return Boolean(entry.backfilled) || (entry.executionStatus && entry.executionStatus !== 'completed');
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
function median(values
|
|
72
|
+
function median(values ) {
|
|
73
73
|
if (!values.length) return null;
|
|
74
74
|
const sorted = [...values].sort((a, b) => a - b);
|
|
75
75
|
const mid = Math.floor(sorted.length / 2);
|
|
@@ -77,11 +77,11 @@ function median(values: number[]): number | null {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
/** One run's rows, walking history in recorded order with a cursor so no interval is counted twice. */
|
|
80
|
-
function runRows(record
|
|
80
|
+
function runRows(record ) {
|
|
81
81
|
const history = Array.isArray(record.history) ? record.history : [];
|
|
82
82
|
let cursor = parseAt(record.startedAt);
|
|
83
|
-
const stages
|
|
84
|
-
const gates
|
|
83
|
+
const stages = [];
|
|
84
|
+
const gates = [];
|
|
85
85
|
let reruns = 0;
|
|
86
86
|
|
|
87
87
|
for (const entry of history) {
|
|
@@ -131,13 +131,13 @@ function runRows(record: any): any {
|
|
|
131
131
|
/** Median elapsed per stage id across runs of one class. Synthetic rows are excluded here and still
|
|
132
132
|
* listed in their run: averaging the moment a file was written into the figure most likely to be
|
|
133
133
|
* quoted would corrupt it. */
|
|
134
|
-
function aggregateStages(runs
|
|
135
|
-
const perStage = new Map
|
|
134
|
+
function aggregateStages(runs ) {
|
|
135
|
+
const perStage = new Map ();
|
|
136
136
|
for (const run of runs) {
|
|
137
137
|
for (const row of run.stages) {
|
|
138
138
|
if (row.synthetic || row.elapsedMs === null || !row.stage) continue;
|
|
139
139
|
if (!perStage.has(row.stage)) perStage.set(row.stage, []);
|
|
140
|
-
perStage.get(row.stage)
|
|
140
|
+
perStage.get(row.stage) .push(row.elapsedMs);
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
143
|
return [...perStage.entries()].map(([stage, values]) => ({
|
|
@@ -146,14 +146,14 @@ function aggregateStages(runs: any[]): any[] {
|
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
/** Pure. Reads nothing and writes nothing; takes already-parsed records. */
|
|
149
|
-
function buildIndicators({ records = [], unreadable = [], exists = true } = {})
|
|
150
|
-
const runs = (records
|
|
151
|
-
const byClass = new Map
|
|
149
|
+
function buildIndicators({ records = [], unreadable = [], exists = true } = {}) {
|
|
150
|
+
const runs = (records ).map(runRows);
|
|
151
|
+
const byClass = new Map ();
|
|
152
152
|
|
|
153
153
|
for (const run of runs) {
|
|
154
154
|
const key = run.taskClass || '(unclassified)';
|
|
155
155
|
if (!byClass.has(key)) byClass.set(key, { taskClass: key, runs: [], stageAggregate: [] });
|
|
156
|
-
byClass.get(key)
|
|
156
|
+
byClass.get(key) .runs.push(run);
|
|
157
157
|
}
|
|
158
158
|
|
|
159
159
|
for (const group of byClass.values()) group.stageAggregate = aggregateStages(group.runs);
|
|
@@ -176,7 +176,7 @@ function buildIndicators({ records = [], unreadable = [], exists = true } = {}):
|
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
/** One class block: its runs, each run's stage and gate rows, then the class median. */
|
|
179
|
-
function printClass(group
|
|
179
|
+
function printClass(group ) {
|
|
180
180
|
console.log('─'.repeat(78));
|
|
181
181
|
console.log(`CLASS: ${group.taskClass} (${group.runs.length} run(s))`);
|
|
182
182
|
for (const run of group.runs) {
|
|
@@ -195,7 +195,7 @@ function printClass(group: any): void {
|
|
|
195
195
|
}
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
-
function handleIndicatorsCommand({ json = false, projectRoot = process.cwd() } = {})
|
|
198
|
+
function handleIndicatorsCommand({ json = false, projectRoot = process.cwd() } = {}) {
|
|
199
199
|
const dir = orchestrationDir(projectRoot);
|
|
200
200
|
const read = readRecords(dir);
|
|
201
201
|
const view = buildIndicators(read);
|
|
@@ -220,7 +220,7 @@ function handleIndicatorsCommand({ json = false, projectRoot = process.cwd() } =
|
|
|
220
220
|
|
|
221
221
|
console.log(`Runs: ${view.runCount} across ${view.byClass.length} task class(es)`);
|
|
222
222
|
if (view.unreadable.length) {
|
|
223
|
-
console.log(`Unreadable: ${view.unreadable.length} record(s) — ${view.unreadable.map((u
|
|
223
|
+
console.log(`Unreadable: ${view.unreadable.length} record(s) — ${view.unreadable.map((u ) => u.file).join(', ')}`);
|
|
224
224
|
}
|
|
225
225
|
|
|
226
226
|
for (const group of view.byClass) printClass(group);
|