@vimhead.dev/norn-cli 0.1.0-tip.35614826665.1 → 0.1.0-tip.35724621678.1
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/assets/README.md +41 -13
- package/assets/docs/README.md +16 -16
- package/assets/docs/agents.md +23 -15
- package/assets/docs/cli.md +10 -8
- package/assets/docs/composition.md +7 -7
- package/assets/docs/persistence.md +22 -22
- package/assets/docs/projects.md +6 -6
- package/assets/docs/recovery.md +10 -10
- package/assets/docs/schemas.md +42 -36
- package/assets/docs/workflows.md +39 -32
- package/assets/examples/agent-then-analysis/input.json +3 -3
- package/assets/examples/agent-then-analysis/norn.project.json +2 -2
- package/assets/examples/agent-then-analysis/plugin.ts +37 -9
- package/assets/examples/caller-owned-routing/README.md +6 -6
- package/assets/examples/caller-owned-routing/assessment.ts +18 -4
- package/assets/examples/caller-owned-routing/contracts.ts +7 -2
- package/assets/examples/caller-owned-routing/input.json +12 -12
- package/assets/examples/caller-owned-routing/norn.project.json +2 -2
- package/assets/examples/caller-owned-routing/revision.ts +7 -2
- package/assets/examples/caller-owned-routing/router.ts +14 -3
- package/assets/examples/caller-selected-continuation/caller.ts +38 -12
- package/assets/examples/caller-selected-continuation/input.json +7 -7
- package/assets/examples/caller-selected-continuation/norn.project.json +2 -2
- package/assets/examples/caller-selected-continuation/producer.ts +10 -3
- package/assets/examples/coordinating-multiple-agents/README.md +6 -6
- package/assets/examples/coordinating-multiple-agents/input.json +20 -8
- package/assets/examples/coordinating-multiple-agents/norn.project.json +2 -2
- package/assets/examples/coordinating-multiple-agents/plugin.ts +165 -46
- package/assets/examples/coordinating-multiple-agents/queue-tools.ts +30 -7
- package/assets/examples/coordinating-multiple-agents/work-queue.ts +211 -63
- package/assets/examples/getting-started/norn.project.json +2 -2
- package/assets/examples/getting-started/plugin.ts +33 -13
- package/assets/examples/minimal-workflow/norn.project.json +2 -2
- package/assets/examples/minimal-workflow/plugin.ts +12 -3
- package/assets/examples/shared-state/README.md +6 -6
- package/assets/examples/shared-state/input.json +1 -1
- package/assets/examples/shared-state/norn.project.json +2 -2
- package/assets/examples/shared-state/plugin.ts +44 -15
- package/assets/examples/shared-state/shared-state.ts +46 -13
- package/assets/examples/shared-state/state-tools.ts +68 -16
- package/assets/examples/worktree-development-loop/README.md +12 -12
- package/assets/examples/worktree-development-loop/norn.project.json +7 -7
- package/assets/examples/worktree-development-loop/plugin.ts +7 -1
- package/assets/examples/worktree-development-loop/scope.ts +4 -1
- package/assets/examples/worktree-development-loop/shared/commands.ts +6 -2
- package/assets/examples/worktree-development-loop/workflows/development-loop/execute.ts +18 -4
- package/assets/examples/worktree-development-loop/workflows/development-loop/index.ts +4 -1
- package/assets/examples/worktree-development-loop/workflows/development-loop/repository.ts +11 -1
- package/assets/examples/worktree-development-loop/workflows/development-loop/schema.ts +6 -2
- package/assets/examples/worktree-development-loop/workflows/implementation/execute.ts +40 -10
- package/assets/examples/worktree-development-loop/workflows/implementation/index.ts +5 -1
- package/assets/examples/worktree-development-loop/workflows/planning/execute.ts +1 -1
- package/assets/examples/worktree-development-loop/workflows/planning/index.ts +5 -1
- package/assets/examples/worktree-development-loop/workflows/review/execute.ts +33 -9
- package/assets/examples/worktree-development-loop/workflows/review-router/execute.ts +51 -8
- package/assets/package.json +1 -1
- package/assets/packages/cli/src/build-info.ts +4 -1
- package/assets/packages/cli/src/bun/cli.ts +21 -5
- package/assets/packages/cli/src/cli.ts +585 -170
- package/assets/packages/cli/src/client.ts +194 -36
- package/assets/packages/cli/src/documentation.ts +135 -30
- package/assets/packages/cli/src/generated-build-info.ts +2 -2
- package/assets/packages/cli/src/internal/agent-directory.ts +7 -2
- package/assets/packages/cli/src/internal/agent-response-tool.ts +57 -15
- package/assets/packages/cli/src/internal/agents.ts +248 -60
- package/assets/packages/cli/src/internal/commands.ts +82 -20
- package/assets/packages/cli/src/internal/documentation-bundle.ts +44 -13
- package/assets/packages/cli/src/internal/engine.ts +396 -87
- package/assets/packages/cli/src/internal/errors.ts +13 -5
- package/assets/packages/cli/src/internal/execution-context.ts +20 -12
- package/assets/packages/cli/src/internal/file-coordinator.ts +95 -26
- package/assets/packages/cli/src/internal/file-names.ts +4 -1
- package/assets/packages/cli/src/internal/launch-request.ts +74 -22
- package/assets/packages/cli/src/internal/logs.ts +10 -2
- package/assets/packages/cli/src/internal/metrics.ts +247 -61
- package/assets/packages/cli/src/internal/pi-assets.ts +105 -23
- package/assets/packages/cli/src/internal/run-lease.ts +72 -22
- package/assets/packages/cli/src/internal/run-log.ts +49 -20
- package/assets/packages/cli/src/internal/run-names.ts +120 -12
- package/assets/packages/cli/src/internal/run-state.ts +306 -71
- package/assets/packages/cli/src/internal/run-store.ts +216 -53
- package/assets/packages/cli/src/internal/usage.ts +23 -7
- package/assets/packages/cli/src/internal/worker-directory.ts +9 -3
- package/assets/packages/cli/src/internal/workflow-registry.ts +281 -70
- package/assets/packages/cli/src/internal/working-directory.ts +2 -1
- package/assets/packages/cli/src/workflow-loader.ts +341 -74
- package/assets/packages/core/src/atomic-files.ts +13 -4
- package/assets/packages/core/src/workflow-transition.ts +13 -2
- package/assets/packages/sdk/src/api.ts +248 -80
- package/assets/packages/sdk/src/index.ts +6 -1
- package/assets/packages/sdk/src/schema.ts +75 -21
- package/assets/tests/workflow-ref.test.ts +242 -57
- package/bin/norn.mjs +13 -4
- package/dist/cli.js +344 -90
- package/dist/client.js +100 -22
- package/dist/documentation.js +102 -25
- package/dist/generated-build-info.d.ts +2 -2
- package/dist/generated-build-info.js +2 -2
- package/dist/internal/agent-directory.js +3 -1
- package/dist/internal/agent-response-tool.js +42 -11
- package/dist/internal/agents.js +149 -37
- package/dist/internal/commands.js +65 -16
- package/dist/internal/documentation-bundle.js +23 -11
- package/dist/internal/engine.js +304 -66
- package/dist/internal/errors.js +4 -2
- package/dist/internal/execution-context.js +2 -1
- package/dist/internal/file-coordinator.js +59 -19
- package/dist/internal/launch-request.js +32 -14
- package/dist/internal/metrics.js +138 -38
- package/dist/internal/pi-assets.js +70 -19
- package/dist/internal/run-lease.js +29 -9
- package/dist/internal/run-log.js +24 -12
- package/dist/internal/run-state.js +172 -48
- package/dist/internal/run-store.js +157 -45
- package/dist/internal/worker-directory.js +6 -2
- package/dist/internal/workflow-registry.js +123 -32
- package/dist/internal/working-directory.js +2 -1
- package/dist/workflow-loader.js +198 -41
- package/package.json +2 -2
|
@@ -5,20 +5,34 @@ import { DatabaseSync } from "node:sqlite";
|
|
|
5
5
|
import type { Static, TSchema } from "typebox";
|
|
6
6
|
import { Value } from "typebox/value";
|
|
7
7
|
|
|
8
|
-
export type SharedStateField<Schema extends TSchema = TSchema> = {
|
|
9
|
-
|
|
8
|
+
export type SharedStateField<Schema extends TSchema = TSchema> = {
|
|
9
|
+
readonly id: string;
|
|
10
|
+
readonly schema: Schema;
|
|
11
|
+
};
|
|
12
|
+
export type SharedStateAccess = Pick<
|
|
13
|
+
SharedState,
|
|
14
|
+
"get" | "getOptional" | "set"
|
|
15
|
+
>;
|
|
10
16
|
|
|
11
17
|
export class SharedState {
|
|
12
18
|
private isClosed = false;
|
|
13
19
|
private constructor(private readonly database: DatabaseSync) {}
|
|
14
20
|
|
|
15
|
-
static async open(input: {
|
|
21
|
+
static async open(input: {
|
|
22
|
+
readonly path: string;
|
|
23
|
+
readonly create: boolean;
|
|
24
|
+
}): Promise<SharedState> {
|
|
16
25
|
if (input.create) await mkdir(dirname(input.path), { recursive: true });
|
|
17
26
|
else await access(input.path);
|
|
18
27
|
const database = new DatabaseSync(input.path);
|
|
19
28
|
try {
|
|
20
|
-
database.exec(
|
|
21
|
-
|
|
29
|
+
database.exec(
|
|
30
|
+
"PRAGMA busy_timeout = 30000; PRAGMA journal_mode = DELETE;",
|
|
31
|
+
);
|
|
32
|
+
if (input.create)
|
|
33
|
+
database.exec(
|
|
34
|
+
"CREATE TABLE IF NOT EXISTS state (key TEXT PRIMARY KEY, value TEXT NOT NULL)",
|
|
35
|
+
);
|
|
22
36
|
database.prepare("SELECT key, value FROM state LIMIT 0").all();
|
|
23
37
|
return new SharedState(database);
|
|
24
38
|
} catch (error) {
|
|
@@ -27,20 +41,36 @@ export class SharedState {
|
|
|
27
41
|
}
|
|
28
42
|
}
|
|
29
43
|
|
|
30
|
-
async get<Schema extends TSchema>(
|
|
44
|
+
async get<Schema extends TSchema>(
|
|
45
|
+
field: SharedStateField<Schema>,
|
|
46
|
+
): Promise<Static<Schema>> {
|
|
31
47
|
const value = await this.getOptional(field);
|
|
32
|
-
if (value === undefined)
|
|
48
|
+
if (value === undefined)
|
|
49
|
+
throw new Error(`Missing shared state: ${field.id}`);
|
|
33
50
|
return value;
|
|
34
51
|
}
|
|
35
52
|
|
|
36
|
-
async getOptional<Schema extends TSchema>(
|
|
37
|
-
|
|
38
|
-
|
|
53
|
+
async getOptional<Schema extends TSchema>(
|
|
54
|
+
field: SharedStateField<Schema>,
|
|
55
|
+
): Promise<Static<Schema> | undefined> {
|
|
56
|
+
const row = this.database
|
|
57
|
+
.prepare("SELECT value FROM state WHERE key = ?")
|
|
58
|
+
.get(field.id);
|
|
59
|
+
return row === undefined
|
|
60
|
+
? undefined
|
|
61
|
+
: copyValue(field.schema, JSON.parse(String(row.value)));
|
|
39
62
|
}
|
|
40
63
|
|
|
41
|
-
async set<Schema extends TSchema>(
|
|
64
|
+
async set<Schema extends TSchema>(
|
|
65
|
+
field: SharedStateField<Schema>,
|
|
66
|
+
value: NoInfer<Static<Schema>>,
|
|
67
|
+
): Promise<void> {
|
|
42
68
|
const checked = copyValue(field.schema, value);
|
|
43
|
-
this.database
|
|
69
|
+
this.database
|
|
70
|
+
.prepare(
|
|
71
|
+
"INSERT INTO state (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value",
|
|
72
|
+
)
|
|
73
|
+
.run(field.id, JSON.stringify(checked));
|
|
44
74
|
}
|
|
45
75
|
|
|
46
76
|
close(): void {
|
|
@@ -50,7 +80,10 @@ export class SharedState {
|
|
|
50
80
|
}
|
|
51
81
|
}
|
|
52
82
|
|
|
53
|
-
function copyValue<Schema extends TSchema>(
|
|
83
|
+
function copyValue<Schema extends TSchema>(
|
|
84
|
+
schema: Schema,
|
|
85
|
+
value: unknown,
|
|
86
|
+
): Static<Schema> {
|
|
54
87
|
Value.Assert(jsonValueSchema, value);
|
|
55
88
|
return structuredClone(Value.Parse(schema, value));
|
|
56
89
|
}
|
|
@@ -4,7 +4,11 @@ import type { ToolDefinition } from "@vimhead.dev/norn";
|
|
|
4
4
|
import type { SharedStateAccess, SharedStateField } from "./shared-state.ts";
|
|
5
5
|
import { inspectSchema } from "@vimhead.dev/norn/schema";
|
|
6
6
|
|
|
7
|
-
function defineTool<Schema extends TSchema>(
|
|
7
|
+
function defineTool<Schema extends TSchema>(
|
|
8
|
+
tool: ToolDefinition<Schema>,
|
|
9
|
+
): ToolDefinition<Schema> {
|
|
10
|
+
return tool;
|
|
11
|
+
}
|
|
8
12
|
|
|
9
13
|
export type StateFieldAccess = {
|
|
10
14
|
readonly field: SharedStateField;
|
|
@@ -12,57 +16,105 @@ export type StateFieldAccess = {
|
|
|
12
16
|
};
|
|
13
17
|
|
|
14
18
|
const pageParameters = {
|
|
15
|
-
offset: Type.Integer({
|
|
19
|
+
offset: Type.Integer({
|
|
20
|
+
minimum: 0,
|
|
21
|
+
description:
|
|
22
|
+
"Zero-based UTF-16 offset into the serialized JSON. Start at 0.",
|
|
23
|
+
}),
|
|
16
24
|
limit: Type.Integer({ minimum: 1, maximum: 10000 }),
|
|
17
25
|
};
|
|
18
26
|
|
|
19
|
-
export function createStateTools(input: {
|
|
27
|
+
export function createStateTools(input: {
|
|
28
|
+
readonly state: SharedStateAccess;
|
|
29
|
+
readonly fields: readonly StateFieldAccess[];
|
|
30
|
+
}): ToolDefinition[] {
|
|
20
31
|
const fields = new Map(input.fields.map((grant) => [grant.field.id, grant]));
|
|
21
|
-
if (fields.size !== input.fields.length || fields.size === 0)
|
|
32
|
+
if (fields.size !== input.fields.length || fields.size === 0)
|
|
33
|
+
throw new Error("State tools require unique, explicitly selected fields");
|
|
22
34
|
const selectField = (key: string, access: "read" | "write") => {
|
|
23
35
|
const grant = fields.get(key);
|
|
24
|
-
if (!grant || (grant.access !== access && grant.access !== "read-write"))
|
|
36
|
+
if (!grant || (grant.access !== access && grant.access !== "read-write"))
|
|
37
|
+
throw new Error(`State ${access} is not attached: ${key}`);
|
|
25
38
|
return grant.field;
|
|
26
39
|
};
|
|
27
40
|
return [
|
|
28
41
|
defineTool({
|
|
29
42
|
name: "norn_state_list",
|
|
30
43
|
label: "Attached workflow state",
|
|
31
|
-
description:
|
|
44
|
+
description: `List only attached workflow-state field IDs, permissions and
|
|
45
|
+
value schemas. JSON is paginated; use nextOffset until null.`,
|
|
32
46
|
parameters: Type.Object(pageParameters),
|
|
33
47
|
async execute(_id, args) {
|
|
34
|
-
return serializePage({
|
|
48
|
+
return serializePage({
|
|
49
|
+
value: [...fields.values()].map(({ field, access }) => ({
|
|
50
|
+
id: field.id,
|
|
51
|
+
access,
|
|
52
|
+
schema: inspectSchema(field.schema),
|
|
53
|
+
})),
|
|
54
|
+
...args,
|
|
55
|
+
});
|
|
35
56
|
},
|
|
36
57
|
}),
|
|
37
58
|
defineTool({
|
|
38
59
|
name: "norn_state_get",
|
|
39
60
|
label: "Read workflow state",
|
|
40
|
-
description:
|
|
61
|
+
description: `Read a selected workflow-state field.
|
|
62
|
+
Unset fields return isSet:false. JSON is paginated; concurrent writes can
|
|
63
|
+
change later pages, so compare revision before combining pages.`,
|
|
41
64
|
parameters: Type.Object({ key: Type.String(), ...pageParameters }),
|
|
42
65
|
async execute(_id, args) {
|
|
43
|
-
const value = await input.state.getOptional(
|
|
44
|
-
|
|
66
|
+
const value = await input.state.getOptional(
|
|
67
|
+
selectField(args.key, "read"),
|
|
68
|
+
);
|
|
69
|
+
return serializePage({
|
|
70
|
+
value:
|
|
71
|
+
value === undefined ? { isSet: false } : { isSet: true, value },
|
|
72
|
+
...args,
|
|
73
|
+
});
|
|
45
74
|
},
|
|
46
75
|
}),
|
|
47
76
|
defineTool({
|
|
48
77
|
name: "norn_state_set",
|
|
49
78
|
label: "Write workflow state",
|
|
50
|
-
description:
|
|
79
|
+
description: `Set an explicitly writable workflow-state field.
|
|
80
|
+
Validate the value against its schema from norn_state_list.
|
|
81
|
+
A get followed by set is not a transaction.`,
|
|
51
82
|
parameters: Type.Object({ key: Type.String(), value: Type.Unknown() }),
|
|
52
83
|
async execute(_id, args, signal) {
|
|
53
84
|
signal?.throwIfAborted();
|
|
54
85
|
const field = selectField(args.key, "write");
|
|
55
86
|
await input.state.set(field, args.value);
|
|
56
|
-
return {
|
|
87
|
+
return {
|
|
88
|
+
content: [{ type: "text", text: "Workflow state saved." }],
|
|
89
|
+
details: {},
|
|
90
|
+
};
|
|
57
91
|
},
|
|
58
92
|
}),
|
|
59
93
|
];
|
|
60
94
|
}
|
|
61
95
|
|
|
62
|
-
function serializePage(input: {
|
|
96
|
+
function serializePage(input: {
|
|
97
|
+
readonly value: unknown;
|
|
98
|
+
readonly offset: number;
|
|
99
|
+
readonly limit: number;
|
|
100
|
+
}) {
|
|
63
101
|
const serialized = JSON.stringify(input.value);
|
|
64
|
-
if (
|
|
102
|
+
if (
|
|
103
|
+
!Number.isInteger(input.offset) ||
|
|
104
|
+
input.offset < 0 ||
|
|
105
|
+
!Number.isInteger(input.limit) ||
|
|
106
|
+
input.limit < 1 ||
|
|
107
|
+
input.limit > 10000
|
|
108
|
+
)
|
|
109
|
+
throw new Error("Invalid state output page");
|
|
65
110
|
const end = Math.min(serialized.length, input.offset + input.limit);
|
|
66
|
-
const details = {
|
|
67
|
-
|
|
111
|
+
const details = {
|
|
112
|
+
text: serialized.slice(input.offset, end),
|
|
113
|
+
nextOffset: end < serialized.length ? end : null,
|
|
114
|
+
revision: createHash("sha256").update(serialized).digest("hex"),
|
|
115
|
+
};
|
|
116
|
+
return {
|
|
117
|
+
content: [{ type: "text" as const, text: JSON.stringify(details) }],
|
|
118
|
+
details,
|
|
119
|
+
};
|
|
68
120
|
}
|
|
@@ -19,13 +19,13 @@ workflow:
|
|
|
19
19
|
|
|
20
20
|
## Source layout
|
|
21
21
|
|
|
22
|
-
| File or directory
|
|
23
|
-
|
|
24
|
-
| [scope.ts](scope.ts)
|
|
25
|
-
| [plugin.ts](plugin.ts)
|
|
26
|
-
| [workflows/development-loop/](workflows/development-loop/)
|
|
27
|
-
| [workflows/planning/](workflows/planning/), [workflows/implementation/](workflows/implementation/), [workflows/review/](workflows/review/) | Define each agent step's inputs and execution.
|
|
28
|
-
| [workflows/review-router/](workflows/review-router/)
|
|
22
|
+
| File or directory | Role in this example |
|
|
23
|
+
| ------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------ |
|
|
24
|
+
| [scope.ts](scope.ts) | Declares the shared namespace and repository configuration. |
|
|
25
|
+
| [plugin.ts](plugin.ts) | Exports the workflows for registration. |
|
|
26
|
+
| [workflows/development-loop/](workflows/development-loop/) | Defines entrypoint inputs and repository setup. |
|
|
27
|
+
| [workflows/planning/](workflows/planning/), [workflows/implementation/](workflows/implementation/), [workflows/review/](workflows/review/) | Define each agent step's inputs and execution. |
|
|
28
|
+
| [workflows/review-router/](workflows/review-router/) | Defines editable gate fields and routes the chosen decision. |
|
|
29
29
|
|
|
30
30
|
## Setup
|
|
31
31
|
|
|
@@ -90,11 +90,11 @@ proof that the task succeeded.
|
|
|
90
90
|
|
|
91
91
|
Only `decision` and `summary` are gate-editable fields:
|
|
92
92
|
|
|
93
|
-
| Decision
|
|
94
|
-
|
|
95
|
-
| `accept`
|
|
96
|
-
| `revise`
|
|
97
|
-
| `blocked` | Fail with outcome data `status: blocked` and the supplied summary.
|
|
93
|
+
| Decision | Result after resume |
|
|
94
|
+
| --------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
|
|
95
|
+
| `accept` | Complete with outcome data `status: done`, including at the iteration limit. |
|
|
96
|
+
| `revise` | Run the next implementation/review iteration, then interrupt again. At `maxIterations`, fail with outcome data `status: needs-attention`. |
|
|
97
|
+
| `blocked` | Fail with outcome data `status: blocked` and the supplied summary. |
|
|
98
98
|
|
|
99
99
|
For an authorized acceptance decision after checking the work:
|
|
100
100
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
"version": 1,
|
|
3
|
+
"workflows": ["./plugin.ts"],
|
|
4
|
+
"config": {
|
|
5
|
+
"worktreeDevelopmentLoop": {
|
|
6
|
+
"repositoryRoot": "/absolute/path/to/source-repository"
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
9
|
}
|
|
@@ -4,4 +4,10 @@ import { implementationWorkflow } from "./workflows/implementation/execute.ts";
|
|
|
4
4
|
import { reviewWorkflow } from "./workflows/review/execute.ts";
|
|
5
5
|
import { reviewRouterWorkflow } from "./workflows/review-router/execute.ts";
|
|
6
6
|
|
|
7
|
-
export default [
|
|
7
|
+
export default [
|
|
8
|
+
developmentLoopWorkflow,
|
|
9
|
+
planningWorkflow,
|
|
10
|
+
implementationWorkflow,
|
|
11
|
+
reviewWorkflow,
|
|
12
|
+
reviewRouterWorkflow,
|
|
13
|
+
];
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { workflowScope } from "@vimhead.dev/norn";
|
|
2
2
|
import { developmentLoopConfigSchema } from "./workflows/development-loop/schema.ts";
|
|
3
3
|
|
|
4
|
-
export const developmentLoopScope = workflowScope({
|
|
4
|
+
export const developmentLoopScope = workflowScope({
|
|
5
|
+
name: "worktreeDevelopmentLoop",
|
|
6
|
+
config: developmentLoopConfigSchema,
|
|
7
|
+
});
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { NornCommandRunResult } from "@vimhead.dev/norn";
|
|
2
2
|
|
|
3
|
-
export async function ensureCommandSucceeded(
|
|
3
|
+
export async function ensureCommandSucceeded(
|
|
4
|
+
result: NornCommandRunResult,
|
|
5
|
+
): Promise<void> {
|
|
4
6
|
if (result.exitCode === 0) return;
|
|
5
|
-
throw new Error(
|
|
7
|
+
throw new Error(
|
|
8
|
+
`${result.label} failed with exit code ${result.exitCode ?? "unknown"}: ${result.stderrTail || result.stdoutTail}`,
|
|
9
|
+
);
|
|
6
10
|
}
|
|
@@ -6,11 +6,25 @@ import { materializeWorkspaceRepository } from "./repository.ts";
|
|
|
6
6
|
|
|
7
7
|
export const developmentLoopWorkflow = developmentLoopScope.workflow({
|
|
8
8
|
name: "developmentLoop",
|
|
9
|
-
entrypoint: {
|
|
9
|
+
entrypoint: {
|
|
10
|
+
instructions: `Use when you want a repository task planned, implemented,
|
|
11
|
+
and reviewed in a separate clone with a caller approval gate. Starts from
|
|
12
|
+
committed content and retains changes for inspection without automatically
|
|
13
|
+
merging or pushing results back. Requires Git, Bash, and model access.`,
|
|
14
|
+
},
|
|
10
15
|
args: developmentLoopArgsSchema,
|
|
11
16
|
async execute({ args, scope, paths, commands }): Promise<WorkflowResult> {
|
|
12
|
-
const repositoryPath = await materializeWorkspaceRepository({
|
|
17
|
+
const repositoryPath = await materializeWorkspaceRepository({
|
|
18
|
+
commands,
|
|
19
|
+
paths,
|
|
20
|
+
repositoryRoot: scope.config.repositoryRoot,
|
|
21
|
+
baseRef: args.baseRef,
|
|
22
|
+
});
|
|
13
23
|
|
|
14
|
-
return planningWorkflow({
|
|
15
|
-
|
|
24
|
+
return planningWorkflow({
|
|
25
|
+
task: args.task,
|
|
26
|
+
repositoryPath,
|
|
27
|
+
maxIterations: args.maxIterations,
|
|
28
|
+
});
|
|
29
|
+
},
|
|
16
30
|
});
|
|
@@ -1,3 +1,6 @@
|
|
|
1
1
|
export { developmentLoopWorkflow } from "./execute.ts";
|
|
2
|
-
export {
|
|
2
|
+
export {
|
|
3
|
+
developmentLoopConfigSchema,
|
|
4
|
+
developmentLoopArgsSchema,
|
|
5
|
+
} from "./schema.ts";
|
|
3
6
|
export type { DevelopmentLoopConfig, DevelopmentLoopArgs } from "./schema.ts";
|
|
@@ -4,7 +4,17 @@ import { ensureCommandSucceeded } from "../../shared/commands.ts";
|
|
|
4
4
|
|
|
5
5
|
const WORKSPACE_REPOSITORY_PATH = "repo";
|
|
6
6
|
|
|
7
|
-
export async function materializeWorkspaceRepository({
|
|
7
|
+
export async function materializeWorkspaceRepository({
|
|
8
|
+
commands,
|
|
9
|
+
paths,
|
|
10
|
+
repositoryRoot,
|
|
11
|
+
baseRef,
|
|
12
|
+
}: {
|
|
13
|
+
commands: NornCommands;
|
|
14
|
+
paths: NornWorkflowPaths;
|
|
15
|
+
repositoryRoot: string;
|
|
16
|
+
baseRef: string;
|
|
17
|
+
}): Promise<string> {
|
|
8
18
|
const repositoryPath = join(paths.workspace, WORKSPACE_REPOSITORY_PATH);
|
|
9
19
|
const result = await commands.run({
|
|
10
20
|
label: "materialize-workspace-repository",
|
|
@@ -10,5 +10,9 @@ export const developmentLoopArgsSchema = Type.Object({
|
|
|
10
10
|
maxIterations: Type.Integer({ minimum: 1, maximum: 10, default: 3 }),
|
|
11
11
|
});
|
|
12
12
|
|
|
13
|
-
export type DevelopmentLoopConfig = StaticDecode<
|
|
14
|
-
|
|
13
|
+
export type DevelopmentLoopConfig = StaticDecode<
|
|
14
|
+
typeof developmentLoopConfigSchema
|
|
15
|
+
>;
|
|
16
|
+
export type DevelopmentLoopArgs = StaticDecode<
|
|
17
|
+
typeof developmentLoopArgsSchema
|
|
18
|
+
>;
|
|
@@ -11,15 +11,28 @@ export const implementationWorkflow = developmentLoopScope.workflow({
|
|
|
11
11
|
name: "implementation",
|
|
12
12
|
entrypoint: false,
|
|
13
13
|
args: implementationArgsSchema,
|
|
14
|
-
async execute({
|
|
14
|
+
async execute({
|
|
15
|
+
args,
|
|
16
|
+
paths,
|
|
17
|
+
agents,
|
|
18
|
+
commands,
|
|
19
|
+
logs,
|
|
20
|
+
}): Promise<WorkflowResult> {
|
|
15
21
|
const repositoryPath = resolve(paths.workspace, args.repositoryPath);
|
|
16
22
|
const plan = await readFile(join(paths.workspace, args.planPath), "utf8");
|
|
17
|
-
const previousReview = args.previousReviewPath
|
|
23
|
+
const previousReview = args.previousReviewPath
|
|
24
|
+
? await readFile(join(paths.workspace, args.previousReviewPath), "utf8")
|
|
25
|
+
: undefined;
|
|
18
26
|
const implementation = await agents.prompt({
|
|
19
27
|
label: `implementation-${args.iteration}`,
|
|
20
28
|
cwd: repositoryPath,
|
|
21
29
|
tools: ["read", "grep", "find", "ls", "edit", "write", "bash"],
|
|
22
|
-
prompt: buildImplementationPrompt(
|
|
30
|
+
prompt: buildImplementationPrompt(
|
|
31
|
+
args.task,
|
|
32
|
+
plan,
|
|
33
|
+
args.iteration,
|
|
34
|
+
previousReview,
|
|
35
|
+
),
|
|
23
36
|
response: implementationAgentResponseSchema,
|
|
24
37
|
});
|
|
25
38
|
const status = await commands.run({
|
|
@@ -30,17 +43,33 @@ export const implementationWorkflow = developmentLoopScope.workflow({
|
|
|
30
43
|
await ensureCommandSucceeded(status);
|
|
31
44
|
const statusOutput = await logs.read(status.stdoutLog);
|
|
32
45
|
await mkdir(join(paths.workspace, "implementation"), { recursive: true });
|
|
33
|
-
await writeFile(
|
|
34
|
-
|
|
35
|
-
|
|
46
|
+
await writeFile(
|
|
47
|
+
join(
|
|
48
|
+
paths.workspace,
|
|
49
|
+
`implementation/iteration-${args.iteration}-status.txt`,
|
|
50
|
+
),
|
|
51
|
+
statusOutput,
|
|
52
|
+
);
|
|
53
|
+
return reviewWorkflow({
|
|
54
|
+
...args,
|
|
55
|
+
implementationSummary: implementation.summary,
|
|
56
|
+
});
|
|
57
|
+
},
|
|
36
58
|
});
|
|
37
59
|
|
|
38
|
-
function buildImplementationPrompt(
|
|
60
|
+
function buildImplementationPrompt(
|
|
61
|
+
task: string,
|
|
62
|
+
plan: string,
|
|
63
|
+
iteration: number,
|
|
64
|
+
previousReview: string | undefined,
|
|
65
|
+
): string {
|
|
39
66
|
return [
|
|
40
67
|
`Implement iteration ${iteration} for this repository task.`,
|
|
41
68
|
"Modify files as needed in the current repository.",
|
|
42
69
|
"Keep changes focused and run a cheap relevant check when possible.",
|
|
43
|
-
previousReview
|
|
70
|
+
previousReview
|
|
71
|
+
? "Address the previous review before making new changes."
|
|
72
|
+
: undefined,
|
|
44
73
|
"",
|
|
45
74
|
"Task:",
|
|
46
75
|
task,
|
|
@@ -50,6 +79,7 @@ function buildImplementationPrompt(task: string, plan: string, iteration: number
|
|
|
50
79
|
previousReview ? "" : undefined,
|
|
51
80
|
previousReview ? "Previous review:" : undefined,
|
|
52
81
|
previousReview,
|
|
53
|
-
]
|
|
82
|
+
]
|
|
83
|
+
.filter((line): line is string => line !== undefined)
|
|
84
|
+
.join("\n");
|
|
54
85
|
}
|
|
55
|
-
|
|
@@ -1,2 +1,6 @@
|
|
|
1
1
|
export { implementationWorkflow } from "./execute.ts";
|
|
2
|
-
export {
|
|
2
|
+
export {
|
|
3
|
+
implementationAgentResponseSchema,
|
|
4
|
+
implementationArgsSchema,
|
|
5
|
+
type ImplementationArgs,
|
|
6
|
+
} from "./schema.ts";
|
|
@@ -23,7 +23,7 @@ export const planningWorkflow = developmentLoopScope.workflow({
|
|
|
23
23
|
await mkdir(join(paths.workspace, "planning"), { recursive: true });
|
|
24
24
|
await writeFile(join(paths.workspace, planPath), planning.plan);
|
|
25
25
|
return implementationWorkflow({ ...args, planPath, iteration: 1 });
|
|
26
|
-
}
|
|
26
|
+
},
|
|
27
27
|
});
|
|
28
28
|
|
|
29
29
|
function buildPlanningPrompt(task: string): string {
|
|
@@ -11,28 +11,46 @@ export const reviewWorkflow = developmentLoopScope.workflow({
|
|
|
11
11
|
name: "review",
|
|
12
12
|
entrypoint: false,
|
|
13
13
|
args: reviewArgsSchema,
|
|
14
|
-
async execute({
|
|
14
|
+
async execute({
|
|
15
|
+
args,
|
|
16
|
+
paths,
|
|
17
|
+
agents,
|
|
18
|
+
commands,
|
|
19
|
+
logs,
|
|
20
|
+
}): Promise<WorkflowResult> {
|
|
15
21
|
const repositoryPath = resolve(paths.workspace, args.repositoryPath);
|
|
16
22
|
const implementationSummary = args.implementationSummary;
|
|
17
23
|
const plan = await readFile(join(paths.workspace, args.planPath), "utf8");
|
|
18
24
|
const diff = await commands.run({
|
|
19
25
|
label: `review-${args.iteration}-diff`,
|
|
20
26
|
cwd: repositoryPath,
|
|
21
|
-
command:
|
|
27
|
+
command:
|
|
28
|
+
"git status --short && git diff --stat HEAD -- . && git diff HEAD -- .",
|
|
22
29
|
});
|
|
23
30
|
await ensureCommandSucceeded(diff);
|
|
24
31
|
const diffOutput = await logs.read(diff.stdoutLog);
|
|
25
32
|
await mkdir(join(paths.workspace, "review"), { recursive: true });
|
|
26
|
-
await writeFile(
|
|
33
|
+
await writeFile(
|
|
34
|
+
join(paths.workspace, `review/iteration-${args.iteration}-diff.txt`),
|
|
35
|
+
diffOutput,
|
|
36
|
+
);
|
|
27
37
|
const review = await agents.prompt({
|
|
28
38
|
label: `review-${args.iteration}`,
|
|
29
39
|
cwd: repositoryPath,
|
|
30
40
|
tools: ["read", "grep", "find", "ls", "bash"],
|
|
31
|
-
prompt: buildReviewPrompt(
|
|
41
|
+
prompt: buildReviewPrompt(
|
|
42
|
+
args.task,
|
|
43
|
+
plan,
|
|
44
|
+
implementationSummary,
|
|
45
|
+
diffOutput,
|
|
46
|
+
),
|
|
32
47
|
response: reviewAgentResponseSchema,
|
|
33
48
|
});
|
|
34
49
|
const automatedReviewPath = `review/iteration-${args.iteration}-automated.json`;
|
|
35
|
-
await writeFile(
|
|
50
|
+
await writeFile(
|
|
51
|
+
join(paths.workspace, automatedReviewPath),
|
|
52
|
+
JSON.stringify(review, null, 2),
|
|
53
|
+
);
|
|
36
54
|
return reviewRouterWorkflow({
|
|
37
55
|
...args,
|
|
38
56
|
iteration: args.iteration,
|
|
@@ -40,13 +58,20 @@ export const reviewWorkflow = developmentLoopScope.workflow({
|
|
|
40
58
|
summary: review.summary,
|
|
41
59
|
automatedReviewPath,
|
|
42
60
|
});
|
|
43
|
-
}
|
|
61
|
+
},
|
|
44
62
|
});
|
|
45
63
|
|
|
46
|
-
function buildReviewPrompt(
|
|
64
|
+
function buildReviewPrompt(
|
|
65
|
+
task: string,
|
|
66
|
+
plan: string,
|
|
67
|
+
implementationSummary: string,
|
|
68
|
+
diff: string,
|
|
69
|
+
): string {
|
|
47
70
|
return [
|
|
48
71
|
"Review the current repository changes against the task and plan.",
|
|
49
|
-
|
|
72
|
+
`Use accept only when the work is ready. Use revise for fixable issues.
|
|
73
|
+
Use blocked when manual input is needed. Include review details in the
|
|
74
|
+
summary.`,
|
|
50
75
|
"",
|
|
51
76
|
"Task:",
|
|
52
77
|
task,
|
|
@@ -61,4 +86,3 @@ function buildReviewPrompt(task: string, plan: string, implementationSummary: st
|
|
|
61
86
|
diff.slice(0, 40_000),
|
|
62
87
|
].join("\n");
|
|
63
88
|
}
|
|
64
|
-
|