@mmerterden/multi-agent-pipeline 20.27.2 → 20.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -0
- package/README.md +20 -6
- package/README.tr.md +6 -5
- package/docs/adr/0008-installer-modularization-and-secret-leak-defense.md +0 -1
- package/docs/architecture.md +4 -4
- package/docs/ecosystem.md +5 -5
- package/docs/facts.json +14 -12
- package/docs/features.md +2 -0
- package/install/_dev-only-files.mjs +1 -0
- package/install/catalog-history.json +1 -1
- package/install/claude.mjs +112 -0
- package/install/index.mjs +0 -2
- package/install/templates/claude-hooks.json +30 -1
- package/manifest.json +101 -67
- package/package.json +4 -1
- package/pipeline/agents/code-reviewer.md +1 -0
- package/pipeline/agents/test-gen-analyst.md +98 -0
- package/pipeline/commands/multi-agent/analysis/SKILL.md +3 -1
- package/pipeline/commands/multi-agent/resume/SKILL.md +2 -1
- package/pipeline/commands/multi-agent/sync/SKILL.md +3 -3
- package/pipeline/commands/multi-agent/test-gen/SKILL.md +124 -0
- package/pipeline/contract/types/index.d.ts +1 -1
- package/pipeline/lib/hook-registration.mjs +252 -0
- package/pipeline/lib/prefs-value.mjs +44 -0
- package/pipeline/lib/run-completion.mjs +148 -0
- package/pipeline/multi-agent-refs/analysis/render.md +4 -4
- package/pipeline/multi-agent-refs/cross-cli-contract.md +11 -5
- package/pipeline/multi-agent-refs/features/doctor.md +24 -0
- package/pipeline/multi-agent-refs/features/test-classification.md +76 -0
- package/pipeline/multi-agent-refs/features/usage-reporting.md +17 -0
- package/pipeline/multi-agent-refs/help/en.md +1 -0
- package/pipeline/multi-agent-refs/help/tr.md +1 -0
- package/pipeline/multi-agent-refs/phases/operations.md +19 -2
- package/pipeline/multi-agent-refs/phases/phase-0-init.md +6 -2
- package/pipeline/multi-agent-refs/phases/phase-1-plan.md +2 -2
- package/pipeline/multi-agent-refs/phases/phase-2-dev.md +7 -8
- package/pipeline/multi-agent-refs/phases/phase-3-review.md +5 -5
- package/pipeline/multi-agent-refs/phases/phase-4-commit.md +7 -6
- package/pipeline/multi-agent-refs/phases/phase-5-report.md +10 -8
- package/pipeline/multi-agent-refs/test-gen/stages.md +227 -0
- package/pipeline/preferences-template.json +10 -0
- package/pipeline/rules/figma-pipeline.md +2 -0
- package/pipeline/schemas/agent-state.schema.json +20 -5
- package/pipeline/schemas/phases.json +3 -2
- package/pipeline/schemas/phases.schema.json +5 -1
- package/pipeline/schemas/prefs.schema.json +51 -0
- package/pipeline/schemas/runs-index.schema.json +1 -1
- package/pipeline/schemas/test-gen-discovery.schema.json +73 -0
- package/pipeline/schemas/test-gen-gaps.schema.json +122 -0
- package/pipeline/schemas/test-gen-inventory.schema.json +59 -0
- package/pipeline/schemas/test-gen-results.schema.json +44 -0
- package/pipeline/scripts/_active-run.mjs +158 -0
- package/pipeline/scripts/_autopilot-outcomes.mjs +1 -0
- package/pipeline/scripts/_guard_rules.py +605 -0
- package/pipeline/scripts/_handle-patterns.mjs +166 -0
- package/pipeline/scripts/_http-post.mjs +141 -0
- package/pipeline/scripts/_stack-routing.mjs +4 -0
- package/pipeline/scripts/_test-gaming.mjs +227 -0
- package/pipeline/scripts/_unified-diff.mjs +104 -0
- package/pipeline/scripts/_worktree-snapshot.mjs +113 -0
- package/pipeline/scripts/agent-guard.py +118 -7
- package/pipeline/scripts/agent-guard.sh +50 -3
- package/pipeline/scripts/argv-secret-scan.mjs +114 -0
- package/pipeline/scripts/autopilot-runner.mjs +8 -2
- package/pipeline/scripts/confirm-command.mjs +70 -0
- package/pipeline/scripts/doctor.mjs +105 -0
- package/pipeline/scripts/evidence-gate.mjs +82 -7
- package/pipeline/scripts/gate-ledger.mjs +163 -7
- package/pipeline/scripts/handle-diff-audit.mjs +188 -0
- package/pipeline/scripts/plan-coverage-gate.mjs +9 -8
- package/pipeline/scripts/pre-commit-check.sh +12 -0
- package/pipeline/scripts/routine-registry.mjs +5 -1
- package/pipeline/scripts/run-scope-guard.mjs +188 -0
- package/pipeline/scripts/run-scope-guard.sh +23 -0
- package/pipeline/scripts/run-stop-guard.mjs +246 -0
- package/pipeline/scripts/run-stop-guard.sh +25 -0
- package/pipeline/scripts/run-ui-tests.sh +27 -3
- package/pipeline/scripts/test-classify.mjs +296 -0
- package/pipeline/scripts/test-gen-gate.mjs +409 -0
- package/pipeline/scripts/test-gen-state.mjs +313 -0
- package/pipeline/scripts/test-integrity-gate.mjs +76 -11
- package/pipeline/scripts/uninstall.mjs +2 -0
- package/pipeline/scripts/usage-register-beacon.mjs +239 -0
- package/pipeline/scripts/usage-register.mjs +128 -33
- package/pipeline/scripts/usage-report.mjs +43 -33
- package/pipeline/scripts/validate-state.mjs +26 -0
- package/pipeline/scripts/write-state.mjs +114 -0
- package/pipeline/skills/.skill-manifest.json +23 -3
- package/pipeline/skills/.skills-index.json +57 -2
- package/pipeline/skills/shared/README.md +10 -5
- package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +3 -3
- package/pipeline/skills/shared/core/multi-agent-test-gen/SKILL.md +120 -0
- package/pipeline/skills/shared/external/fix-review-verdicts/SKILL.md +133 -0
- package/pipeline/skills/shared/external/handle-request/SKILL.md +97 -0
- package/pipeline/skills/shared/external/scenario-gap-analysis/SKILL.md +129 -0
- package/pipeline/skills/shared/external/scenario-gap-analysis/references/scenario-schema.md +60 -0
- package/pipeline/skills/shared/external/test-result-triage/SKILL.md +160 -0
- package/pipeline/skills/skills-index.md +7 -2
- package/install/_telemetry.mjs +0 -61
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://github.com/mmerterden/multi-agent-pipeline/pipeline/schemas/test-gen-gaps.schema.json",
|
|
4
|
+
"title": "Multi-Agent Pipeline - test-gen gaps",
|
|
5
|
+
"description": "The gap analysis of /multi-agent:test-gen, $RUN/gaps.json (multi-agent-refs/test-gen/stages.md). Written from the read-only analyst's answer: the selected inventory items set against the existing tests, each gap a scenario to write. `wrongly-covered` (a test exists but cannot see the failure) is kept apart from `missing`. dataNeeds[] says what test data a scenario needs and who provides it; it never holds a value, because values are entered by a person or resolved by secret reference at run time. unmeasured[] lists what the analysis could not measure, which is not the same as covered. test-gen-gate.mjs check --stage gaps checks this shape without a dependency.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": ["schemaVersion", "coverage", "gaps", "dataNeeds", "unmeasured"],
|
|
9
|
+
"$defs": {
|
|
10
|
+
"fileLine": { "type": "string", "pattern": "^[^\\s:]+:[1-9][0-9]*$" }
|
|
11
|
+
},
|
|
12
|
+
"properties": {
|
|
13
|
+
"schemaVersion": { "const": "1.0.0" },
|
|
14
|
+
"coverage": {
|
|
15
|
+
"type": "object",
|
|
16
|
+
"additionalProperties": false,
|
|
17
|
+
"required": ["method", "measured"],
|
|
18
|
+
"properties": {
|
|
19
|
+
"method": {
|
|
20
|
+
"enum": ["coverage_gap", "agent"],
|
|
21
|
+
"description": "coverage_gap: the toolkit tool joined inventory and tests by identifier; agent: the analyst's own reading, on a toolkit without the tool."
|
|
22
|
+
},
|
|
23
|
+
"measured": { "type": "boolean" },
|
|
24
|
+
"reason": { "type": "string", "minLength": 1 },
|
|
25
|
+
"covered": { "type": "integer", "minimum": 0 },
|
|
26
|
+
"uncovered": { "type": "integer", "minimum": 0 },
|
|
27
|
+
"unmatched": { "type": "integer", "minimum": 0 }
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"gaps": {
|
|
31
|
+
"type": "array",
|
|
32
|
+
"items": {
|
|
33
|
+
"type": "object",
|
|
34
|
+
"additionalProperties": false,
|
|
35
|
+
"required": [
|
|
36
|
+
"id",
|
|
37
|
+
"inventoryId",
|
|
38
|
+
"bucket",
|
|
39
|
+
"title",
|
|
40
|
+
"priority",
|
|
41
|
+
"rationale",
|
|
42
|
+
"expectation",
|
|
43
|
+
"automation",
|
|
44
|
+
"method"
|
|
45
|
+
],
|
|
46
|
+
"properties": {
|
|
47
|
+
"id": { "type": "string", "minLength": 1 },
|
|
48
|
+
"inventoryId": { "type": "string", "minLength": 1 },
|
|
49
|
+
"bucket": { "enum": ["missing", "wrongly-covered"] },
|
|
50
|
+
"title": { "type": "string", "minLength": 1 },
|
|
51
|
+
"priority": {
|
|
52
|
+
"enum": ["bug-catching", "edge", "basic"],
|
|
53
|
+
"description": "bug-catching: would catch a failure users hit; edge: a boundary or error path; basic: the happy path."
|
|
54
|
+
},
|
|
55
|
+
"rationale": {
|
|
56
|
+
"type": "string",
|
|
57
|
+
"minLength": 1,
|
|
58
|
+
"description": "Why the business cares."
|
|
59
|
+
},
|
|
60
|
+
"expectation": {
|
|
61
|
+
"type": "string",
|
|
62
|
+
"minLength": 1,
|
|
63
|
+
"description": "What the test asserts, measurable."
|
|
64
|
+
},
|
|
65
|
+
"automation": { "enum": ["yes", "no", "needs-code"] },
|
|
66
|
+
"method": {
|
|
67
|
+
"enum": ["exact", "inferred"],
|
|
68
|
+
"description": "exact: matched by identifier; inferred: the analyst's judgement, kept apart."
|
|
69
|
+
},
|
|
70
|
+
"evidence": { "type": "array", "items": { "$ref": "#/$defs/fileLine" } },
|
|
71
|
+
"handlesNeeded": {
|
|
72
|
+
"type": "array",
|
|
73
|
+
"items": {
|
|
74
|
+
"type": "object",
|
|
75
|
+
"additionalProperties": false,
|
|
76
|
+
"required": ["target", "suggestion"],
|
|
77
|
+
"properties": {
|
|
78
|
+
"target": { "type": "string", "minLength": 1 },
|
|
79
|
+
"suggestion": { "type": "string", "minLength": 1 }
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
"dataNeeds": {
|
|
87
|
+
"type": "array",
|
|
88
|
+
"items": {
|
|
89
|
+
"type": "object",
|
|
90
|
+
"additionalProperties": false,
|
|
91
|
+
"required": ["id", "gapIds", "what", "kind"],
|
|
92
|
+
"properties": {
|
|
93
|
+
"id": { "type": "string", "minLength": 1 },
|
|
94
|
+
"gapIds": {
|
|
95
|
+
"type": "array",
|
|
96
|
+
"minItems": 1,
|
|
97
|
+
"items": { "type": "string", "minLength": 1 }
|
|
98
|
+
},
|
|
99
|
+
"what": { "type": "string", "minLength": 1 },
|
|
100
|
+
"kind": { "enum": ["account", "seed", "fixture", "secret", "other"] },
|
|
101
|
+
"secretRef": {
|
|
102
|
+
"type": "string",
|
|
103
|
+
"minLength": 1,
|
|
104
|
+
"description": "Credential name a test resolves at run time; never the value."
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
"unmeasured": {
|
|
110
|
+
"type": "array",
|
|
111
|
+
"items": {
|
|
112
|
+
"type": "object",
|
|
113
|
+
"additionalProperties": false,
|
|
114
|
+
"required": ["reason"],
|
|
115
|
+
"properties": {
|
|
116
|
+
"inventoryId": { "type": "string", "minLength": 1 },
|
|
117
|
+
"reason": { "type": "string", "minLength": 1 }
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://github.com/mmerterden/multi-agent-pipeline/pipeline/schemas/test-gen-inventory.schema.json",
|
|
4
|
+
"title": "Multi-Agent Pipeline - test-gen inventory",
|
|
5
|
+
"description": "What the app has that a test could cover, written by the inventory stage of /multi-agent:test-gen as $RUN/inventory.json (multi-agent-refs/test-gen/stages.md). Items are joined to tests by their stable id (route, screen id, Figma node id, endpoint path), never by title. `priority` is added after the inventory, from the user's pick or, in autopilot, the top three. test-gen-gate.mjs check --stage inventory checks this shape without a dependency.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": ["schemaVersion", "platform", "source", "items"],
|
|
9
|
+
"properties": {
|
|
10
|
+
"schemaVersion": { "const": "1.0.0" },
|
|
11
|
+
"platform": { "enum": ["ios", "android", "web", "api"] },
|
|
12
|
+
"repo": { "type": "string", "minLength": 1 },
|
|
13
|
+
"source": {
|
|
14
|
+
"enum": ["design_scenario_inventory", "web_map", "ui_tree", "openapi", "agent"],
|
|
15
|
+
"description": "Where the items came from. `agent` is the stage's own reading of the code, used when no tool answered."
|
|
16
|
+
},
|
|
17
|
+
"generatedAt": { "type": "string", "format": "date-time" },
|
|
18
|
+
"items": {
|
|
19
|
+
"type": "array",
|
|
20
|
+
"minItems": 1,
|
|
21
|
+
"items": {
|
|
22
|
+
"type": "object",
|
|
23
|
+
"additionalProperties": false,
|
|
24
|
+
"required": ["id", "kind", "title"],
|
|
25
|
+
"properties": {
|
|
26
|
+
"id": { "type": "string", "minLength": 1, "description": "Stable identifier." },
|
|
27
|
+
"kind": { "enum": ["screen", "flow", "endpoint", "component"] },
|
|
28
|
+
"title": { "type": "string", "minLength": 1 },
|
|
29
|
+
"path": {
|
|
30
|
+
"type": "string",
|
|
31
|
+
"minLength": 1,
|
|
32
|
+
"description": "Code path that implements it, file or directory."
|
|
33
|
+
},
|
|
34
|
+
"handles": {
|
|
35
|
+
"type": "array",
|
|
36
|
+
"items": { "type": "string", "minLength": 1 },
|
|
37
|
+
"description": "Automation handles already on it (accessibilityIdentifier, testTag, data-testid)."
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"priority": {
|
|
43
|
+
"type": "object",
|
|
44
|
+
"additionalProperties": false,
|
|
45
|
+
"required": ["mode", "selected"],
|
|
46
|
+
"properties": {
|
|
47
|
+
"mode": {
|
|
48
|
+
"enum": ["user", "autopilot-top3"],
|
|
49
|
+
"description": "user: picked by the person; autopilot-top3: the three the run proceeded with unattended."
|
|
50
|
+
},
|
|
51
|
+
"selected": {
|
|
52
|
+
"type": "array",
|
|
53
|
+
"minItems": 1,
|
|
54
|
+
"items": { "type": "string", "minLength": 1 }
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://github.com/mmerterden/multi-agent-pipeline/pipeline/schemas/test-gen-results.schema.json",
|
|
4
|
+
"title": "Multi-Agent Pipeline - test-gen results",
|
|
5
|
+
"description": "The outcome of the tests /multi-agent:test-gen wrote, $RUN/results.json, built by test-gen-gate.mjs results from the classify stage (multi-agent-refs/features/test-classification.md). Three states: pass, fail, not_measured; anything but pass carries its reason. `classification` keeps the measured class behind the state (an unstable test is a fail, a test with no base is not_measured), and `triage` the result-triage verdict once one is made. test-gen-gate.mjs check --stage results checks this shape without a dependency.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": ["schemaVersion", "source", "green", "tests", "summary"],
|
|
9
|
+
"properties": {
|
|
10
|
+
"schemaVersion": { "const": "1.0.0" },
|
|
11
|
+
"source": { "enum": ["toolkit", "fallback"] },
|
|
12
|
+
"green": { "type": "boolean" },
|
|
13
|
+
"tests": {
|
|
14
|
+
"type": "array",
|
|
15
|
+
"items": {
|
|
16
|
+
"type": "object",
|
|
17
|
+
"additionalProperties": false,
|
|
18
|
+
"required": ["id", "status"],
|
|
19
|
+
"properties": {
|
|
20
|
+
"id": { "type": "string", "minLength": 1 },
|
|
21
|
+
"file": { "type": "string", "minLength": 1 },
|
|
22
|
+
"status": { "enum": ["pass", "fail", "not_measured"] },
|
|
23
|
+
"classification": {
|
|
24
|
+
"enum": ["passed", "regression", "preexisting", "unstable", "not_measured"]
|
|
25
|
+
},
|
|
26
|
+
"reason": { "type": "string", "minLength": 1 },
|
|
27
|
+
"triage": { "enum": ["product-bug", "stale-test", "new-scenario", "valid", "ambiguous"] }
|
|
28
|
+
},
|
|
29
|
+
"if": { "properties": { "status": { "enum": ["fail", "not_measured"] } } },
|
|
30
|
+
"then": { "required": ["reason"] }
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"summary": {
|
|
34
|
+
"type": "object",
|
|
35
|
+
"additionalProperties": false,
|
|
36
|
+
"required": ["pass", "fail", "not_measured"],
|
|
37
|
+
"properties": {
|
|
38
|
+
"pass": { "type": "integer", "minimum": 0 },
|
|
39
|
+
"fail": { "type": "integer", "minimum": 0 },
|
|
40
|
+
"not_measured": { "type": "integer", "minimum": 0 }
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file _active-run.mjs - which pipeline run a hook call belongs to, and where
|
|
3
|
+
* that run stands against its mode's phase list.
|
|
4
|
+
*
|
|
5
|
+
* A hook receives the host's session id and working directory, never a task id.
|
|
6
|
+
* A run is bound to a session by one of three facts, strongest first:
|
|
7
|
+
*
|
|
8
|
+
* 1. `sessionId` equals MULTI_AGENT_SESSION_ID in the hook's environment: the
|
|
9
|
+
* id a launcher started the run with (continuous mode, a client).
|
|
10
|
+
* 2. `hostSessionId` equals the payload's `session_id`: write-state.mjs stamps
|
|
11
|
+
* the host session that made the run's last phase transition
|
|
12
|
+
* (CLAUDE_CODE_SESSION_ID), so the session driving a run is the one bound
|
|
13
|
+
* to it, and a resume in a new session rebinds it at its first transition.
|
|
14
|
+
* 3. A state with no `hostSessionId` whose `worktreePath` is the git toplevel
|
|
15
|
+
* of the payload's cwd: a run written before the stamp existed. A state
|
|
16
|
+
* that carries a different `hostSessionId` is another session's run and
|
|
17
|
+
* never matches this way.
|
|
18
|
+
*
|
|
19
|
+
* Nothing here decides anything; callers (run-stop-guard, run-scope-guard)
|
|
20
|
+
* apply their own rule to the run this returns. Zero runtime dependencies.
|
|
21
|
+
*
|
|
22
|
+
* @module pipeline/scripts/_active-run
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { spawnSync } from "node:child_process";
|
|
26
|
+
import { appendFileSync, mkdirSync, realpathSync, statSync } from "node:fs";
|
|
27
|
+
import { dirname, join } from "node:path";
|
|
28
|
+
|
|
29
|
+
import { readRegularJson } from "../lib/regular-file.mjs";
|
|
30
|
+
import { listRuns, logsRoot, resolveRunFile } from "./_run-paths.mjs";
|
|
31
|
+
|
|
32
|
+
export {
|
|
33
|
+
finalPhaseOf,
|
|
34
|
+
modeOf,
|
|
35
|
+
modePhases,
|
|
36
|
+
nextPhaseOf,
|
|
37
|
+
phaseContract,
|
|
38
|
+
phaseName,
|
|
39
|
+
} from "../lib/run-completion.mjs";
|
|
40
|
+
|
|
41
|
+
function real(p) {
|
|
42
|
+
try {
|
|
43
|
+
return realpathSync(p);
|
|
44
|
+
} catch {
|
|
45
|
+
return p;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The git toplevel of `dir`, real-pathed, or null outside a repository. */
|
|
50
|
+
export function gitToplevel(dir) {
|
|
51
|
+
if (!dir) return null;
|
|
52
|
+
const r = spawnSync("git", ["-C", dir, "rev-parse", "--show-toplevel"], {
|
|
53
|
+
encoding: "utf8",
|
|
54
|
+
timeout: 5000,
|
|
55
|
+
});
|
|
56
|
+
return r.status === 0 ? real(r.stdout.trim()) : null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function mtime(p) {
|
|
60
|
+
try {
|
|
61
|
+
return statSync(p).mtimeMs;
|
|
62
|
+
} catch {
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const LIVE_STATUSES = new Set(["in_progress", "awaiting_input"]);
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The run bound to this hook call, or null.
|
|
71
|
+
*
|
|
72
|
+
* @param {{sessionId?: string|null, cwd?: string|null, env?: Record<string,string|undefined>}} opts
|
|
73
|
+
* @returns {{statePath: string, state: object, matchedBy: "session"|"host-session"|"worktree"} | null}
|
|
74
|
+
* with `unreadable` (count of state files that did not parse) on the returned
|
|
75
|
+
* object or on the `findActiveRun.lastUnreadable` property when nothing matched
|
|
76
|
+
*/
|
|
77
|
+
export function findActiveRun({ sessionId = null, cwd = null, env = process.env } = {}) {
|
|
78
|
+
const launched = env.MULTI_AGENT_SESSION_ID || "";
|
|
79
|
+
let top;
|
|
80
|
+
const topOf = () => {
|
|
81
|
+
if (top === undefined) top = cwd ? gitToplevel(cwd) : null;
|
|
82
|
+
return top;
|
|
83
|
+
};
|
|
84
|
+
const hits = [];
|
|
85
|
+
let unreadable = 0;
|
|
86
|
+
const prevRoot = process.env.LOGS_ROOT;
|
|
87
|
+
if (env.LOGS_ROOT !== undefined) process.env.LOGS_ROOT = env.LOGS_ROOT;
|
|
88
|
+
try {
|
|
89
|
+
for (const run of listRuns()) {
|
|
90
|
+
const p = resolveRunFile(run.taskId, "agent-state.json", run.project);
|
|
91
|
+
if (!p) continue;
|
|
92
|
+
const doc = readRegularJson(p, null);
|
|
93
|
+
if (!doc || typeof doc !== "object" || Array.isArray(doc)) {
|
|
94
|
+
unreadable += 1;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
let matchedBy = null;
|
|
98
|
+
if (launched && doc.sessionId === launched) matchedBy = "session";
|
|
99
|
+
else if (sessionId && doc.hostSessionId === sessionId) matchedBy = "host-session";
|
|
100
|
+
else if (
|
|
101
|
+
!doc.hostSessionId &&
|
|
102
|
+
typeof doc.worktreePath === "string" &&
|
|
103
|
+
doc.worktreePath &&
|
|
104
|
+
topOf() &&
|
|
105
|
+
real(doc.worktreePath) === topOf()
|
|
106
|
+
) {
|
|
107
|
+
matchedBy = "worktree";
|
|
108
|
+
}
|
|
109
|
+
if (matchedBy) hits.push({ statePath: p, state: doc, matchedBy, t: mtime(p) });
|
|
110
|
+
}
|
|
111
|
+
} finally {
|
|
112
|
+
if (prevRoot === undefined) delete process.env.LOGS_ROOT;
|
|
113
|
+
else process.env.LOGS_ROOT = prevRoot;
|
|
114
|
+
}
|
|
115
|
+
findActiveRun.lastUnreadable = unreadable;
|
|
116
|
+
if (!hits.length) return null;
|
|
117
|
+
const rank = { session: 0, "host-session": 1, worktree: 2 };
|
|
118
|
+
hits.sort((a, b) => rank[a.matchedBy] - rank[b.matchedBy] || b.t - a.t);
|
|
119
|
+
const lead = hits.filter((h) => h.matchedBy === hits[0].matchedBy);
|
|
120
|
+
// A session can hold a finished run next to a live one. The run started
|
|
121
|
+
// last is the one the session is on: a live run started after the finished
|
|
122
|
+
// one wins however recently the finished file was touched, and a live run
|
|
123
|
+
// started before it was abandoned without a halt and does not hold the
|
|
124
|
+
// session. startedAt orders them; the state's mtime stands in without it.
|
|
125
|
+
const started = (h) => Date.parse(h.state?.startedAt) || h.t;
|
|
126
|
+
const isLive = (h) => LIVE_STATUSES.has(h.state?.status);
|
|
127
|
+
const liveRun = lead.find(isLive);
|
|
128
|
+
const finished = lead.filter((h) => !isLive(h));
|
|
129
|
+
const lastFinished = finished.sort((a, b) => started(b) - started(a))[0];
|
|
130
|
+
let pick = lead[0];
|
|
131
|
+
if (liveRun)
|
|
132
|
+
pick = lastFinished && started(lastFinished) > started(liveRun) ? lastFinished : liveRun;
|
|
133
|
+
const { t: _t, ...best } = pick;
|
|
134
|
+
return { ...best, unreadable };
|
|
135
|
+
}
|
|
136
|
+
findActiveRun.lastUnreadable = 0;
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Append one line to the hook failure log under the log root. A hook that
|
|
140
|
+
* fails open says why somewhere a person can read later; this never throws.
|
|
141
|
+
*
|
|
142
|
+
* @param {string} hook
|
|
143
|
+
* @param {string} reason
|
|
144
|
+
* @param {Record<string,string|undefined>} [env]
|
|
145
|
+
*/
|
|
146
|
+
export function logHookFailure(hook, reason, env = process.env) {
|
|
147
|
+
try {
|
|
148
|
+
const root = env.LOGS_ROOT || logsRoot();
|
|
149
|
+
mkdirSync(root, { recursive: true });
|
|
150
|
+
const line = `${new Date().toISOString()} ${hook}: ${String(reason).replace(/\s+/g, " ")}\n`;
|
|
151
|
+
appendFileSync(join(root, "hook-failures.log"), line);
|
|
152
|
+
} catch {
|
|
153
|
+
// Logging is best-effort; the hook's verdict never depends on it.
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** The run directory that holds a state file. */
|
|
158
|
+
export const runDirOf = (statePath) => dirname(statePath);
|
|
@@ -27,6 +27,7 @@ export const OUTCOME = Object.freeze({
|
|
|
27
27
|
MERGED: "merged",
|
|
28
28
|
SKIPPED: "skipped",
|
|
29
29
|
COMPLETED_NO_PR: "completed-no-pr",
|
|
30
|
+
STOPPED_INCOMPLETE: "stopped-incomplete",
|
|
30
31
|
NEEDS_INPUT: "needs-input",
|
|
31
32
|
AWAITING_ANSWER: "awaiting-answer",
|
|
32
33
|
VERIFICATION_FAILED: "verification-failed",
|