astrocode-workflow 0.1.57 → 0.1.59
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/README.md +243 -11
- package/dist/agents/prompts.d.ts +1 -0
- package/dist/agents/prompts.js +159 -0
- package/dist/agents/registry.js +11 -1
- package/dist/config/loader.js +34 -0
- package/dist/config/schema.d.ts +12 -1
- package/dist/config/schema.js +14 -0
- package/dist/hooks/continuation-enforcer.d.ts +9 -1
- package/dist/hooks/continuation-enforcer.js +2 -1
- package/dist/hooks/inject-provider.d.ts +9 -1
- package/dist/hooks/inject-provider.js +11 -3
- package/dist/hooks/tool-output-truncator.d.ts +9 -1
- package/dist/hooks/tool-output-truncator.js +2 -1
- package/dist/index.js +228 -45
- package/dist/state/adapters/index.d.ts +4 -2
- package/dist/state/adapters/index.js +23 -27
- package/dist/state/db.d.ts +6 -8
- package/dist/state/db.js +106 -45
- package/dist/tools/index.d.ts +13 -3
- package/dist/tools/index.js +14 -31
- package/dist/tools/init.d.ts +10 -1
- package/dist/tools/init.js +73 -18
- package/dist/tools/spec.d.ts +0 -1
- package/dist/tools/spec.js +4 -1
- package/dist/tools/status.d.ts +1 -1
- package/dist/tools/status.js +70 -52
- package/dist/tools/workflow.js +6 -3
- package/dist/workflow/directives.d.ts +2 -0
- package/dist/workflow/directives.js +34 -19
- package/dist/workflow/state-machine.d.ts +27 -0
- package/dist/workflow/state-machine.js +167 -86
- package/package.json +1 -1
- package/src/agents/prompts.ts +160 -0
- package/src/agents/registry.ts +16 -1
- package/src/config/loader.ts +39 -4
- package/src/config/schema.ts +16 -0
- package/src/hooks/continuation-enforcer.ts +9 -2
- package/src/hooks/inject-provider.ts +18 -4
- package/src/hooks/tool-output-truncator.ts +9 -2
- package/src/index.ts +260 -56
- package/src/state/adapters/index.ts +21 -26
- package/src/state/db.ts +114 -58
- package/src/tools/index.ts +29 -31
- package/src/tools/init.ts +91 -22
- package/src/tools/spec.ts +6 -2
- package/src/tools/status.ts +71 -55
- package/src/tools/workflow.ts +7 -4
- package/src/workflow/directives.ts +103 -75
- package/src/workflow/state-machine.ts +229 -105
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
import { sha256Hex } from "../shared/hash";
|
|
2
2
|
import { clampChars, normalizeNewlines } from "../shared/text";
|
|
3
|
+
function getInjectMaxChars(config) {
|
|
4
|
+
// Deterministic fallback for older configs.
|
|
5
|
+
const v = config?.context_compaction?.inject_max_chars;
|
|
6
|
+
return typeof v === "number" && Number.isFinite(v) && v > 0 ? v : 12000;
|
|
7
|
+
}
|
|
3
8
|
export function directiveHash(body) {
|
|
4
9
|
// Stable hash to dedupe: normalize newlines + trim
|
|
5
10
|
const norm = normalizeNewlines(body).trim();
|
|
6
11
|
return sha256Hex(norm);
|
|
7
12
|
}
|
|
13
|
+
function finalizeBody(body, maxChars) {
|
|
14
|
+
// Normalize first, clamp second, trim last => hash/body match exactly.
|
|
15
|
+
const norm = normalizeNewlines(body);
|
|
16
|
+
const clamped = clampChars(norm, maxChars);
|
|
17
|
+
return clamped.trim();
|
|
18
|
+
}
|
|
8
19
|
export function buildContinueDirective(opts) {
|
|
9
20
|
const { config, run_id, stage_key, next_action, context_snapshot_md } = opts;
|
|
10
|
-
const
|
|
21
|
+
const maxChars = getInjectMaxChars(config);
|
|
22
|
+
const body = finalizeBody([
|
|
11
23
|
`[SYSTEM DIRECTIVE: ASTROCODE — CONTINUE]`,
|
|
12
24
|
``,
|
|
13
25
|
`This directive is injected by the Astro agent to continue the workflow.`,
|
|
@@ -22,8 +34,8 @@ export function buildContinueDirective(opts) {
|
|
|
22
34
|
`- If blocked, ask exactly ONE question and stop.`,
|
|
23
35
|
``,
|
|
24
36
|
`Context snapshot:`,
|
|
25
|
-
context_snapshot_md.trim(),
|
|
26
|
-
].join("\n")
|
|
37
|
+
(context_snapshot_md ?? "").trim(),
|
|
38
|
+
].join("\n"), maxChars);
|
|
27
39
|
return {
|
|
28
40
|
kind: "continue",
|
|
29
41
|
title: "ASTROCODE — CONTINUE",
|
|
@@ -32,8 +44,9 @@ export function buildContinueDirective(opts) {
|
|
|
32
44
|
};
|
|
33
45
|
}
|
|
34
46
|
export function buildBlockedDirective(opts) {
|
|
35
|
-
const { run_id, stage_key, question, context_snapshot_md } = opts;
|
|
36
|
-
const
|
|
47
|
+
const { config, run_id, stage_key, question, context_snapshot_md } = opts;
|
|
48
|
+
const maxChars = getInjectMaxChars(config);
|
|
49
|
+
const body = finalizeBody([
|
|
37
50
|
`[SYSTEM DIRECTIVE: ASTROCODE — BLOCKED]`,
|
|
38
51
|
``,
|
|
39
52
|
`This directive is injected by the Astro agent indicating the workflow is blocked.`,
|
|
@@ -45,8 +58,8 @@ export function buildBlockedDirective(opts) {
|
|
|
45
58
|
`Question: ${question}`,
|
|
46
59
|
``,
|
|
47
60
|
`Context snapshot:`,
|
|
48
|
-
context_snapshot_md.trim(),
|
|
49
|
-
].join("\n"))
|
|
61
|
+
(context_snapshot_md ?? "").trim(),
|
|
62
|
+
].join("\n"), maxChars);
|
|
50
63
|
return {
|
|
51
64
|
kind: "blocked",
|
|
52
65
|
title: "ASTROCODE — BLOCKED",
|
|
@@ -55,7 +68,8 @@ export function buildBlockedDirective(opts) {
|
|
|
55
68
|
};
|
|
56
69
|
}
|
|
57
70
|
export function buildRepairDirective(opts) {
|
|
58
|
-
const
|
|
71
|
+
const maxChars = getInjectMaxChars(opts.config);
|
|
72
|
+
const body = finalizeBody([
|
|
59
73
|
`[SYSTEM DIRECTIVE: ASTROCODE — REPAIR]`,
|
|
60
74
|
``,
|
|
61
75
|
`This directive is injected by the Astro agent after performing a repair pass.`,
|
|
@@ -63,8 +77,8 @@ export function buildRepairDirective(opts) {
|
|
|
63
77
|
`Astrocode performed a repair pass. Summarize in <= 10 lines and continue.`,
|
|
64
78
|
``,
|
|
65
79
|
`Repair report:`,
|
|
66
|
-
opts.report_md.trim(),
|
|
67
|
-
].join("\n"))
|
|
80
|
+
(opts.report_md ?? "").trim(),
|
|
81
|
+
].join("\n"), maxChars);
|
|
68
82
|
return {
|
|
69
83
|
kind: "repair",
|
|
70
84
|
title: "ASTROCODE — REPAIR",
|
|
@@ -73,12 +87,13 @@ export function buildRepairDirective(opts) {
|
|
|
73
87
|
};
|
|
74
88
|
}
|
|
75
89
|
export function buildStageDirective(opts) {
|
|
76
|
-
const { config, stage_key, run_id, story_key, story_title, stage_agent_name, stage_goal, stage_constraints, context_snapshot_md } = opts;
|
|
77
|
-
const
|
|
78
|
-
const
|
|
90
|
+
const { config, stage_key, run_id, story_key, story_title, stage_agent_name, stage_goal, stage_constraints, context_snapshot_md, } = opts;
|
|
91
|
+
const maxChars = getInjectMaxChars(config);
|
|
92
|
+
const stageKeyUpper = String(stage_key).toUpperCase();
|
|
93
|
+
const constraintsBlock = Array.isArray(stage_constraints) && stage_constraints.length
|
|
79
94
|
? ["", "Constraints:", ...stage_constraints.map((c) => `- ${c}`)].join("\n")
|
|
80
95
|
: "";
|
|
81
|
-
const body =
|
|
96
|
+
const body = finalizeBody([
|
|
82
97
|
`[SYSTEM DIRECTIVE: ASTROCODE — STAGE_${stageKeyUpper}]`,
|
|
83
98
|
``,
|
|
84
99
|
`This directive is injected by the Astro agent to delegate the stage task.`,
|
|
@@ -93,14 +108,14 @@ export function buildStageDirective(opts) {
|
|
|
93
108
|
`Output contract (strict):`,
|
|
94
109
|
`1) Baton markdown (short, structured)`,
|
|
95
110
|
`2) ASTRO JSON between markers:`,
|
|
96
|
-
`
|
|
111
|
+
` <!-- ASTRO_JSON_BEGIN -->`,
|
|
97
112
|
` {`,
|
|
98
113
|
` "schema_version": 1,`,
|
|
99
114
|
` "stage_key": "${stage_key}",`,
|
|
100
115
|
` "status": "ok",`,
|
|
101
|
-
`
|
|
116
|
+
` "...": "..."`,
|
|
102
117
|
` }`,
|
|
103
|
-
`
|
|
118
|
+
` <!-- ASTRO_JSON_END -->`,
|
|
104
119
|
``,
|
|
105
120
|
`ASTRO JSON requirements:`,
|
|
106
121
|
`- stage_key must be "${stage_key}"`,
|
|
@@ -111,8 +126,8 @@ export function buildStageDirective(opts) {
|
|
|
111
126
|
`If blocked: ask exactly ONE question and stop.`,
|
|
112
127
|
``,
|
|
113
128
|
`Context snapshot:`,
|
|
114
|
-
context_snapshot_md.trim(),
|
|
115
|
-
].join("\n")
|
|
129
|
+
(context_snapshot_md ?? "").trim(),
|
|
130
|
+
].join("\n"), maxChars);
|
|
116
131
|
return {
|
|
117
132
|
kind: "stage",
|
|
118
133
|
title: `ASTROCODE — STAGE_${stageKeyUpper}`,
|
|
@@ -1,6 +1,33 @@
|
|
|
1
1
|
import type { AstrocodeConfig } from "../config/schema";
|
|
2
2
|
import type { SqliteDb } from "../state/db";
|
|
3
3
|
import type { RunRow, StageKey, StageRunRow, StoryRow } from "../state/types";
|
|
4
|
+
export declare const EVENT_TYPES: {
|
|
5
|
+
readonly RUN_STARTED: "run.started";
|
|
6
|
+
readonly RUN_COMPLETED: "run.completed";
|
|
7
|
+
readonly RUN_FAILED: "run.failed";
|
|
8
|
+
readonly RUN_ABORTED: "run.aborted";
|
|
9
|
+
readonly RUN_GENESIS_PLANNING_ATTACHED: "run.genesis_planning_attached";
|
|
10
|
+
readonly STAGE_STARTED: "stage.started";
|
|
11
|
+
readonly WORKFLOW_PROCEED: "workflow.proceed";
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* PLANNING-FIRST REDESIGN
|
|
15
|
+
* ----------------------
|
|
16
|
+
* Old behavior: mutate an approved story into a planning/decomposition instruction.
|
|
17
|
+
* New behavior: NEVER mutate story title/body.
|
|
18
|
+
*
|
|
19
|
+
* Planning-first is a run-scoped directive stored in `injects` (scope = run:<run_id>).
|
|
20
|
+
*
|
|
21
|
+
* Deterministic trigger (config-driven):
|
|
22
|
+
* - config.workflow.genesis_planning:
|
|
23
|
+
* - "off" => never attach directive
|
|
24
|
+
* - "first_story_only"=> attach only when story_key === "S-0001"
|
|
25
|
+
* - "always" => attach for every run
|
|
26
|
+
*
|
|
27
|
+
* Contract: DB is already initialized before workflow is used:
|
|
28
|
+
* - schema tables exist
|
|
29
|
+
* - repo_state singleton row (id=1) exists
|
|
30
|
+
*/
|
|
4
31
|
export type NextAction = {
|
|
5
32
|
kind: "idle";
|
|
6
33
|
reason: "no_approved_stories";
|
|
@@ -1,6 +1,17 @@
|
|
|
1
|
+
import { withTx } from "../state/db";
|
|
1
2
|
import { nowISO } from "../shared/time";
|
|
2
3
|
import { newEventId, newRunId, newStageRunId } from "../state/ids";
|
|
3
4
|
import { warn } from "../shared/log";
|
|
5
|
+
import { sha256Hex } from "../shared/hash";
|
|
6
|
+
export const EVENT_TYPES = {
|
|
7
|
+
RUN_STARTED: "run.started",
|
|
8
|
+
RUN_COMPLETED: "run.completed",
|
|
9
|
+
RUN_FAILED: "run.failed",
|
|
10
|
+
RUN_ABORTED: "run.aborted",
|
|
11
|
+
RUN_GENESIS_PLANNING_ATTACHED: "run.genesis_planning_attached",
|
|
12
|
+
STAGE_STARTED: "stage.started",
|
|
13
|
+
WORKFLOW_PROCEED: "workflow.proceed",
|
|
14
|
+
};
|
|
4
15
|
export function getActiveRun(db) {
|
|
5
16
|
const row = db
|
|
6
17
|
.prepare("SELECT * FROM runs WHERE status = 'running' ORDER BY started_at DESC, created_at DESC LIMIT 1")
|
|
@@ -30,9 +41,8 @@ export function decideNextAction(db, config) {
|
|
|
30
41
|
}
|
|
31
42
|
const stageRuns = getStageRuns(db, activeRun.run_id);
|
|
32
43
|
const current = getCurrentStageRun(stageRuns);
|
|
33
|
-
if (!current)
|
|
44
|
+
if (!current)
|
|
34
45
|
return { kind: "complete_run", run_id: activeRun.run_id };
|
|
35
|
-
}
|
|
36
46
|
if (current.status === "pending") {
|
|
37
47
|
return { kind: "delegate_stage", run_id: activeRun.run_id, stage_key: current.stage_key, stage_run_id: current.stage_run_id };
|
|
38
48
|
}
|
|
@@ -42,103 +52,174 @@ export function decideNextAction(db, config) {
|
|
|
42
52
|
if (current.status === "failed") {
|
|
43
53
|
return { kind: "failed", run_id: activeRun.run_id, stage_key: current.stage_key, error_text: current.error_text ?? "stage failed" };
|
|
44
54
|
}
|
|
45
|
-
// Should never happen: other statuses are handled above
|
|
46
55
|
warn("Unexpected stage status in decideNextAction", { status: current.status, stage_key: current.stage_key });
|
|
47
56
|
return { kind: "await_stage_completion", run_id: activeRun.run_id, stage_key: current.stage_key, stage_run_id: current.stage_run_id };
|
|
48
57
|
}
|
|
49
|
-
function
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
58
|
+
function getPipelineFromConfig(config) {
|
|
59
|
+
const pipeline = config?.workflow?.pipeline;
|
|
60
|
+
if (!Array.isArray(pipeline) || pipeline.length === 0) {
|
|
61
|
+
throw new Error("Invalid config: workflow.pipeline must be a non-empty array of stage keys.");
|
|
62
|
+
}
|
|
63
|
+
return pipeline;
|
|
53
64
|
}
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
if (
|
|
57
|
-
|
|
58
|
-
if (
|
|
59
|
-
|
|
60
|
-
|
|
65
|
+
function getGenesisPlanningMode(config) {
|
|
66
|
+
const raw = config?.workflow?.genesis_planning;
|
|
67
|
+
if (raw === "off" || raw === "first_story_only" || raw === "always")
|
|
68
|
+
return raw;
|
|
69
|
+
if (raw != null)
|
|
70
|
+
warn(`Invalid workflow.genesis_planning config: ${String(raw)}. Using default "first_story_only".`);
|
|
71
|
+
return "first_story_only";
|
|
72
|
+
}
|
|
73
|
+
function shouldAttachPlanningDirective(config, story) {
|
|
74
|
+
const mode = getGenesisPlanningMode(config);
|
|
75
|
+
if (mode === "off")
|
|
76
|
+
return false;
|
|
77
|
+
if (mode === "always")
|
|
78
|
+
return true;
|
|
79
|
+
return story.story_key === "S-0001";
|
|
80
|
+
}
|
|
81
|
+
function attachRunPlanningDirective(db, runId, story, pipeline) {
|
|
61
82
|
const now = nowISO();
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
83
|
+
const injectId = `inj_${runId}_genesis_plan`;
|
|
84
|
+
const body = [
|
|
85
|
+
`# Genesis planning directive`,
|
|
86
|
+
``,
|
|
87
|
+
`This run is configured to perform a planning/decomposition pass before implementation.`,
|
|
88
|
+
`Do not edit the origin story title/body. Create additional stories instead.`,
|
|
89
|
+
``,
|
|
90
|
+
`## Required output`,
|
|
91
|
+
`- Produce 50–200 granular implementation stories with clear acceptance criteria.`,
|
|
92
|
+
`- Each story: single focused change, explicit done conditions, dependencies listed.`,
|
|
93
|
+
``,
|
|
94
|
+
`## Context`,
|
|
95
|
+
`- Origin story: ${story.story_key} — ${story.title ?? ""}`,
|
|
96
|
+
`- Pipeline: ${pipeline.join(" → ")}`,
|
|
97
|
+
``,
|
|
98
|
+
].join("\n");
|
|
99
|
+
let hash = null;
|
|
100
|
+
try {
|
|
101
|
+
hash = sha256Hex(body);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
// Hash is optional; directive must never be blocked by hashing.
|
|
105
|
+
hash = null;
|
|
74
106
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
107
|
+
try {
|
|
108
|
+
// Do not clobber user edits. If it exists, we leave it.
|
|
109
|
+
db.prepare(`
|
|
110
|
+
INSERT OR IGNORE INTO injects (
|
|
111
|
+
inject_id, type, title, body_md, tags_json, scope, source, priority,
|
|
112
|
+
expires_at, sha256, created_at, updated_at
|
|
113
|
+
) VALUES (
|
|
114
|
+
?, 'note', ?, ?, '["genesis","planning","decompose"]', ?, 'tool', 100,
|
|
115
|
+
NULL, ?, ?, ?
|
|
116
|
+
)
|
|
117
|
+
`).run(injectId, "Genesis planning: decompose into stories", body, `run:${runId}`, hash, now, now);
|
|
118
|
+
db.prepare("INSERT INTO events (event_id, run_id, stage_key, type, body_json, created_at) VALUES (?, ?, NULL, ?, ?, ?)").run(newEventId(), runId, EVENT_TYPES.RUN_GENESIS_PLANNING_ATTACHED, JSON.stringify({ story_key: story.story_key, inject_id: injectId }), now);
|
|
119
|
+
}
|
|
120
|
+
catch (e) {
|
|
121
|
+
// Helpful, never required for correctness.
|
|
122
|
+
warn("Failed to attach genesis planning inject", { run_id: runId, story_key: story.story_key, err: e });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function updateRepoStateLastEvent(db, now, fields) {
|
|
126
|
+
// Contract: repo_state row exists. If not, fail deterministically (don’t “bootstrap” here).
|
|
127
|
+
const res = db
|
|
128
|
+
.prepare(`
|
|
129
|
+
UPDATE repo_state
|
|
130
|
+
SET last_run_id = COALESCE(?, last_run_id),
|
|
131
|
+
last_story_key = COALESCE(?, last_story_key),
|
|
132
|
+
last_event_at = ?,
|
|
133
|
+
updated_at = ?
|
|
134
|
+
WHERE id = 1
|
|
135
|
+
`)
|
|
136
|
+
.run(fields.last_run_id ?? null, fields.last_story_key ?? null, now, now);
|
|
137
|
+
if (!res || res.changes === 0) {
|
|
138
|
+
throw new Error("repo_state missing (id=1). Database not initialized; run init must be performed before workflow.");
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
export function createRunForStory(db, config, storyKey) {
|
|
142
|
+
return withTx(db, () => {
|
|
143
|
+
const story = getStory(db, storyKey);
|
|
144
|
+
if (!story)
|
|
145
|
+
throw new Error(`Story not found: ${storyKey}`);
|
|
146
|
+
if (story.state !== "approved")
|
|
147
|
+
throw new Error(`Story must be approved to run: ${storyKey} (state=${story.state})`);
|
|
148
|
+
const run_id = newRunId();
|
|
149
|
+
const now = nowISO();
|
|
150
|
+
const pipeline = getPipelineFromConfig(config);
|
|
151
|
+
db.prepare("UPDATE stories SET state='in_progress', in_progress=1, locked_by_run_id=?, locked_at=?, updated_at=? WHERE story_key=?").run(run_id, now, now, storyKey);
|
|
152
|
+
db.prepare("INSERT INTO runs (run_id, story_key, status, pipeline_stages_json, current_stage_key, created_at, started_at, updated_at) VALUES (?, ?, 'running', ?, ?, ?, ?, ?)").run(run_id, storyKey, JSON.stringify(pipeline), pipeline[0] ?? null, now, now, now);
|
|
153
|
+
const insertStage = db.prepare("INSERT INTO stage_runs (stage_run_id, run_id, stage_key, stage_index, status, created_at, updated_at) VALUES (?, ?, ?, ?, 'pending', ?, ?)");
|
|
154
|
+
pipeline.forEach((stageKey, idx) => {
|
|
155
|
+
insertStage.run(newStageRunId(), run_id, stageKey, idx, now, now);
|
|
156
|
+
});
|
|
157
|
+
db.prepare("INSERT INTO events (event_id, run_id, stage_key, type, body_json, created_at) VALUES (?, ?, NULL, ?, ?, ?)").run(newEventId(), run_id, EVENT_TYPES.RUN_STARTED, JSON.stringify({ story_key: storyKey, pipeline }), now);
|
|
158
|
+
if (shouldAttachPlanningDirective(config, story)) {
|
|
159
|
+
attachRunPlanningDirective(db, run_id, story, pipeline);
|
|
160
|
+
}
|
|
161
|
+
updateRepoStateLastEvent(db, now, { last_run_id: run_id, last_story_key: storyKey });
|
|
162
|
+
return { run_id };
|
|
82
163
|
});
|
|
83
|
-
// Event
|
|
84
|
-
db.prepare("INSERT INTO events (event_id, run_id, stage_key, type, body_json, created_at) VALUES (?, ?, NULL, 'run.started', ?, ?)").run(newEventId(), run_id, JSON.stringify({ story_key: storyKey, pipeline }), now);
|
|
85
|
-
db.prepare("UPDATE repo_state SET last_run_id=?, last_story_key=?, last_event_at=?, updated_at=? WHERE id=1").run(run_id, storyKey, now, now);
|
|
86
|
-
return { run_id };
|
|
87
164
|
}
|
|
88
165
|
export function startStage(db, runId, stageKey, meta) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
166
|
+
return withTx(db, () => {
|
|
167
|
+
const now = nowISO();
|
|
168
|
+
const run = db.prepare("SELECT * FROM runs WHERE run_id=?").get(runId);
|
|
169
|
+
if (!run)
|
|
170
|
+
throw new Error(`Run not found: ${runId}`);
|
|
171
|
+
if (run.status !== "running")
|
|
172
|
+
throw new Error(`Run is not running: ${runId} (status=${run.status})`);
|
|
173
|
+
const stage = db.prepare("SELECT * FROM stage_runs WHERE run_id=? AND stage_key=?").get(runId, stageKey);
|
|
174
|
+
if (!stage)
|
|
175
|
+
throw new Error(`Stage run not found: ${runId}/${stageKey}`);
|
|
176
|
+
if (stage.status !== "pending")
|
|
177
|
+
throw new Error(`Stage is not pending: ${stageKey} (status=${stage.status})`);
|
|
178
|
+
db.prepare("UPDATE stage_runs SET status='running', started_at=?, updated_at=?, subagent_type=COALESCE(?, subagent_type), subagent_session_id=COALESCE(?, subagent_session_id) WHERE stage_run_id=?").run(now, now, meta?.subagent_type ?? null, meta?.subagent_session_id ?? null, stage.stage_run_id);
|
|
179
|
+
db.prepare("UPDATE runs SET current_stage_key=?, updated_at=? WHERE run_id=?").run(stageKey, now, runId);
|
|
180
|
+
db.prepare("INSERT INTO events (event_id, run_id, stage_key, type, body_json, created_at) VALUES (?, ?, ?, ?, ?, ?)").run(newEventId(), runId, stageKey, EVENT_TYPES.STAGE_STARTED, JSON.stringify({ subagent_type: meta?.subagent_type ?? null }), now);
|
|
181
|
+
updateRepoStateLastEvent(db, now, {});
|
|
182
|
+
});
|
|
106
183
|
}
|
|
107
184
|
export function completeRun(db, runId) {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
185
|
+
return withTx(db, () => {
|
|
186
|
+
const now = nowISO();
|
|
187
|
+
const run = db.prepare("SELECT * FROM runs WHERE run_id=?").get(runId);
|
|
188
|
+
if (!run)
|
|
189
|
+
throw new Error(`Run not found: ${runId}`);
|
|
190
|
+
if (run.status !== "running")
|
|
191
|
+
throw new Error(`Run not running: ${runId} (status=${run.status})`);
|
|
192
|
+
const stageRuns = getStageRuns(db, runId);
|
|
193
|
+
const incomplete = stageRuns.find((s) => s.status !== "completed" && s.status !== "skipped");
|
|
194
|
+
if (incomplete)
|
|
195
|
+
throw new Error(`Cannot complete run: stage ${incomplete.stage_key} is ${incomplete.status}`);
|
|
196
|
+
db.prepare("UPDATE runs SET status='completed', completed_at=?, updated_at=?, current_stage_key=NULL WHERE run_id=?").run(now, now, runId);
|
|
197
|
+
db.prepare("UPDATE stories SET state='done', in_progress=0, locked_by_run_id=NULL, locked_at=NULL, updated_at=? WHERE story_key=?").run(now, run.story_key);
|
|
198
|
+
db.prepare("INSERT INTO events (event_id, run_id, stage_key, type, body_json, created_at) VALUES (?, ?, NULL, ?, ?, ?)").run(newEventId(), runId, EVENT_TYPES.RUN_COMPLETED, JSON.stringify({ story_key: run.story_key }), now);
|
|
199
|
+
updateRepoStateLastEvent(db, now, {});
|
|
200
|
+
});
|
|
123
201
|
}
|
|
124
202
|
export function failRun(db, runId, stageKey, errorText) {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
203
|
+
return withTx(db, () => {
|
|
204
|
+
const now = nowISO();
|
|
205
|
+
const run = db.prepare("SELECT * FROM runs WHERE run_id=?").get(runId);
|
|
206
|
+
if (!run)
|
|
207
|
+
throw new Error(`Run not found: ${runId}`);
|
|
208
|
+
db.prepare("UPDATE runs SET status='failed', error_text=?, updated_at=?, completed_at=? WHERE run_id=?").run(errorText, now, now, runId);
|
|
209
|
+
db.prepare("UPDATE stories SET state='blocked', in_progress=0, locked_by_run_id=NULL, locked_at=NULL, updated_at=? WHERE story_key=?").run(now, run.story_key);
|
|
210
|
+
db.prepare("INSERT INTO events (event_id, run_id, stage_key, type, body_json, created_at) VALUES (?, ?, ?, ?, ?, ?)").run(newEventId(), runId, stageKey, EVENT_TYPES.RUN_FAILED, JSON.stringify({ error_text: errorText }), now);
|
|
211
|
+
updateRepoStateLastEvent(db, now, {});
|
|
212
|
+
});
|
|
133
213
|
}
|
|
134
214
|
export function abortRun(db, runId, reason) {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
215
|
+
return withTx(db, () => {
|
|
216
|
+
const now = nowISO();
|
|
217
|
+
const run = db.prepare("SELECT * FROM runs WHERE run_id=?").get(runId);
|
|
218
|
+
if (!run)
|
|
219
|
+
throw new Error(`Run not found: ${runId}`);
|
|
220
|
+
db.prepare("UPDATE runs SET status='aborted', error_text=?, updated_at=?, completed_at=? WHERE run_id=?").run(reason, now, now, runId);
|
|
221
|
+
db.prepare("UPDATE stories SET state='approved', in_progress=0, locked_by_run_id=NULL, locked_at=NULL, updated_at=? WHERE story_key=?").run(now, run.story_key);
|
|
222
|
+
db.prepare("INSERT INTO events (event_id, run_id, stage_key, type, body_json, created_at) VALUES (?, ?, NULL, ?, ?, ?)").run(newEventId(), runId, EVENT_TYPES.RUN_ABORTED, JSON.stringify({ reason }), now);
|
|
223
|
+
updateRepoStateLastEvent(db, now, {});
|
|
224
|
+
});
|
|
144
225
|
}
|
package/package.json
CHANGED
package/src/agents/prompts.ts
CHANGED
|
@@ -72,3 +72,163 @@ I analyzed the requirements and identified key components for implementation.
|
|
|
72
72
|
|
|
73
73
|
If blocked, set status="blocked" and add ONE question to questions array. Do not deviate from this format.
|
|
74
74
|
`;
|
|
75
|
+
|
|
76
|
+
export const QA_AGENT_PROMPT = `🌍 Global Engineering Review Prompt (LOCKED)
|
|
77
|
+
|
|
78
|
+
Use this prompt when reviewing any codebase.
|
|
79
|
+
|
|
80
|
+
1️⃣ How every file review starts (MANDATORY)
|
|
81
|
+
|
|
82
|
+
Before discussing details, always answer:
|
|
83
|
+
|
|
84
|
+
Simple question this file answers
|
|
85
|
+
|
|
86
|
+
What decision, contract, or responsibility does this file define?
|
|
87
|
+
|
|
88
|
+
Things you have at the end of running this code
|
|
89
|
+
|
|
90
|
+
What objects, capabilities, state, guarantees, or invariants now exist?
|
|
91
|
+
|
|
92
|
+
If you can't answer these clearly, the file is already suspect.
|
|
93
|
+
|
|
94
|
+
2️⃣ Canonical RULE set (GLOBAL ENGINEERING INVARIANTS)
|
|
95
|
+
|
|
96
|
+
These are engineering physics laws.
|
|
97
|
+
Every serious bug maps to one of these.
|
|
98
|
+
No ad-hoc rules are allowed.
|
|
99
|
+
|
|
100
|
+
enum RULE {
|
|
101
|
+
CAPABILITY_GUARANTEE =
|
|
102
|
+
"Expose a capability only when you can guarantee it will execute safely under current runtime conditions.",
|
|
103
|
+
|
|
104
|
+
RECOVERY_STRICTER =
|
|
105
|
+
"Recovery/degraded paths must be simpler and more conservative than the normal path, and must not introduce new failure modes.",
|
|
106
|
+
|
|
107
|
+
SOURCE_OF_TRUTH =
|
|
108
|
+
"For any piece of state, define exactly one authoritative source and explicit precedence rules for any mirrors, caches, or derivations.",
|
|
109
|
+
|
|
110
|
+
LIFECYCLE_DETERMINISM =
|
|
111
|
+
"Initialization and lifecycle must be deterministic: single construction, stable ordering, controlled side effects, and repeatable outcomes.",
|
|
112
|
+
|
|
113
|
+
SECURITY_BOUNDARIES =
|
|
114
|
+
"Security, authorization, and trust boundaries must be explicit, enforced, and never inferred implicitly."
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
If an issue does not violate one of these rules, it is not a P0/P1 blocker.
|
|
118
|
+
|
|
119
|
+
3️⃣ Severity model (WHAT "P" MEANS)
|
|
120
|
+
|
|
121
|
+
Severity is about trust, not annoyance.
|
|
122
|
+
|
|
123
|
+
P0 — Trust break
|
|
124
|
+
|
|
125
|
+
Unsafe execution
|
|
126
|
+
|
|
127
|
+
Corrupted or ambiguous state
|
|
128
|
+
|
|
129
|
+
Non-deterministic lifecycle
|
|
130
|
+
|
|
131
|
+
Broken auditability / resumability
|
|
132
|
+
|
|
133
|
+
Security boundary violations
|
|
134
|
+
|
|
135
|
+
P1 — Reliability break
|
|
136
|
+
|
|
137
|
+
Runtime crashes after successful boot
|
|
138
|
+
|
|
139
|
+
Capabilities exposed but unusable
|
|
140
|
+
|
|
141
|
+
Degraded mode that lies or half-works
|
|
142
|
+
|
|
143
|
+
Recovery paths that add fragility
|
|
144
|
+
|
|
145
|
+
P2 — Quality / polish
|
|
146
|
+
|
|
147
|
+
Readability, ergonomics, maintainability
|
|
148
|
+
|
|
149
|
+
No RULE violated
|
|
150
|
+
|
|
151
|
+
4️⃣ Mandatory P0 / P1 Blocker Format (STRICT)
|
|
152
|
+
|
|
153
|
+
Every P0 / P1 must be written exactly like this:
|
|
154
|
+
|
|
155
|
+
P{0|1} — <short human title>
|
|
156
|
+
|
|
157
|
+
Rule: RULE.<ONE_ENUM_VALUE>
|
|
158
|
+
|
|
159
|
+
Description:
|
|
160
|
+
A human-readable explanation of how this specific code violates the rule in context.
|
|
161
|
+
This is situational and concrete — not a rule.
|
|
162
|
+
|
|
163
|
+
What:
|
|
164
|
+
The exact defect or unsafe behavior.
|
|
165
|
+
|
|
166
|
+
Where:
|
|
167
|
+
Precise file + function + construct / lines.
|
|
168
|
+
|
|
169
|
+
Proposed fix:
|
|
170
|
+
The smallest possible change that restores the rule.
|
|
171
|
+
(Code snippets if helpful.)
|
|
172
|
+
|
|
173
|
+
Why:
|
|
174
|
+
How this fix restores the invariant and what class of failures it prevents.
|
|
175
|
+
|
|
176
|
+
5️⃣ Recovery / Degraded Mode Lens (AUTO-APPLIED)
|
|
177
|
+
|
|
178
|
+
Whenever code introduces:
|
|
179
|
+
|
|
180
|
+
limited mode
|
|
181
|
+
|
|
182
|
+
fallback
|
|
183
|
+
|
|
184
|
+
catch-based recovery
|
|
185
|
+
|
|
186
|
+
partial initialization
|
|
187
|
+
|
|
188
|
+
Automatically evaluate against:
|
|
189
|
+
|
|
190
|
+
RULE.RECOVERY_STRICTER
|
|
191
|
+
|
|
192
|
+
RULE.CAPABILITY_GUARANTEE
|
|
193
|
+
|
|
194
|
+
RULE.LIFECYCLE_DETERMINISM
|
|
195
|
+
|
|
196
|
+
If recovery adds logic, validation, or ambiguity → it is a blocker.
|
|
197
|
+
|
|
198
|
+
Recovery must:
|
|
199
|
+
|
|
200
|
+
reduce capability surface
|
|
201
|
+
|
|
202
|
+
fail earlier, not later
|
|
203
|
+
|
|
204
|
+
be simpler than the normal path
|
|
205
|
+
|
|
206
|
+
provide a clear path back to normal
|
|
207
|
+
|
|
208
|
+
6️⃣ How to ask for the next file (TEACHING MODE)
|
|
209
|
+
|
|
210
|
+
Before asking for the next file, always explain:
|
|
211
|
+
|
|
212
|
+
What this next file likely does (human-readable)
|
|
213
|
+
|
|
214
|
+
"This file takes X, registers it with Y, then enforces Z..."
|
|
215
|
+
|
|
216
|
+
Why this file matters next
|
|
217
|
+
|
|
218
|
+
Which RULE it is likely to uphold or violate, and why reviewing it now reduces risk.
|
|
219
|
+
|
|
220
|
+
7️⃣ What this frame teaches over time
|
|
221
|
+
|
|
222
|
+
After repeated use, you stop seeing "random bugs" and start seeing patterns:
|
|
223
|
+
|
|
224
|
+
CAPABILITY_GUARANTEE violations (exposed but unsafe APIs)
|
|
225
|
+
|
|
226
|
+
RECOVERY_STRICTER violations (clever fallbacks that explode)
|
|
227
|
+
|
|
228
|
+
SOURCE_OF_TRUTH drift (DB vs disk vs memory)
|
|
229
|
+
|
|
230
|
+
LIFECYCLE_DETERMINISM failures (double init, racey wiring)
|
|
231
|
+
|
|
232
|
+
SECURITY_BOUNDARIES leaks (implicit trust)
|
|
233
|
+
|
|
234
|
+
At that point, reviews become portable skills, not project-specific knowledge.`;
|