@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.
Files changed (119) hide show
  1. package/assets/README.md +41 -13
  2. package/assets/docs/README.md +16 -16
  3. package/assets/docs/agents.md +23 -15
  4. package/assets/docs/cli.md +10 -8
  5. package/assets/docs/composition.md +7 -7
  6. package/assets/docs/persistence.md +22 -22
  7. package/assets/docs/projects.md +6 -6
  8. package/assets/docs/recovery.md +10 -10
  9. package/assets/docs/schemas.md +42 -36
  10. package/assets/docs/workflows.md +39 -32
  11. package/assets/examples/agent-then-analysis/input.json +3 -3
  12. package/assets/examples/agent-then-analysis/norn.project.json +2 -2
  13. package/assets/examples/agent-then-analysis/plugin.ts +37 -9
  14. package/assets/examples/caller-owned-routing/README.md +6 -6
  15. package/assets/examples/caller-owned-routing/assessment.ts +18 -4
  16. package/assets/examples/caller-owned-routing/contracts.ts +7 -2
  17. package/assets/examples/caller-owned-routing/input.json +12 -12
  18. package/assets/examples/caller-owned-routing/norn.project.json +2 -2
  19. package/assets/examples/caller-owned-routing/revision.ts +7 -2
  20. package/assets/examples/caller-owned-routing/router.ts +14 -3
  21. package/assets/examples/caller-selected-continuation/caller.ts +38 -12
  22. package/assets/examples/caller-selected-continuation/input.json +7 -7
  23. package/assets/examples/caller-selected-continuation/norn.project.json +2 -2
  24. package/assets/examples/caller-selected-continuation/producer.ts +10 -3
  25. package/assets/examples/coordinating-multiple-agents/README.md +6 -6
  26. package/assets/examples/coordinating-multiple-agents/input.json +20 -8
  27. package/assets/examples/coordinating-multiple-agents/norn.project.json +2 -2
  28. package/assets/examples/coordinating-multiple-agents/plugin.ts +165 -46
  29. package/assets/examples/coordinating-multiple-agents/queue-tools.ts +30 -7
  30. package/assets/examples/coordinating-multiple-agents/work-queue.ts +211 -63
  31. package/assets/examples/getting-started/norn.project.json +2 -2
  32. package/assets/examples/getting-started/plugin.ts +33 -13
  33. package/assets/examples/minimal-workflow/norn.project.json +2 -2
  34. package/assets/examples/minimal-workflow/plugin.ts +12 -3
  35. package/assets/examples/shared-state/README.md +6 -6
  36. package/assets/examples/shared-state/input.json +1 -1
  37. package/assets/examples/shared-state/norn.project.json +2 -2
  38. package/assets/examples/shared-state/plugin.ts +44 -15
  39. package/assets/examples/shared-state/shared-state.ts +46 -13
  40. package/assets/examples/shared-state/state-tools.ts +68 -16
  41. package/assets/examples/worktree-development-loop/README.md +12 -12
  42. package/assets/examples/worktree-development-loop/norn.project.json +7 -7
  43. package/assets/examples/worktree-development-loop/plugin.ts +7 -1
  44. package/assets/examples/worktree-development-loop/scope.ts +4 -1
  45. package/assets/examples/worktree-development-loop/shared/commands.ts +6 -2
  46. package/assets/examples/worktree-development-loop/workflows/development-loop/execute.ts +18 -4
  47. package/assets/examples/worktree-development-loop/workflows/development-loop/index.ts +4 -1
  48. package/assets/examples/worktree-development-loop/workflows/development-loop/repository.ts +11 -1
  49. package/assets/examples/worktree-development-loop/workflows/development-loop/schema.ts +6 -2
  50. package/assets/examples/worktree-development-loop/workflows/implementation/execute.ts +40 -10
  51. package/assets/examples/worktree-development-loop/workflows/implementation/index.ts +5 -1
  52. package/assets/examples/worktree-development-loop/workflows/planning/execute.ts +1 -1
  53. package/assets/examples/worktree-development-loop/workflows/planning/index.ts +5 -1
  54. package/assets/examples/worktree-development-loop/workflows/review/execute.ts +33 -9
  55. package/assets/examples/worktree-development-loop/workflows/review-router/execute.ts +51 -8
  56. package/assets/package.json +1 -1
  57. package/assets/packages/cli/src/build-info.ts +4 -1
  58. package/assets/packages/cli/src/bun/cli.ts +21 -5
  59. package/assets/packages/cli/src/cli.ts +585 -170
  60. package/assets/packages/cli/src/client.ts +194 -36
  61. package/assets/packages/cli/src/documentation.ts +135 -30
  62. package/assets/packages/cli/src/generated-build-info.ts +2 -2
  63. package/assets/packages/cli/src/internal/agent-directory.ts +7 -2
  64. package/assets/packages/cli/src/internal/agent-response-tool.ts +57 -15
  65. package/assets/packages/cli/src/internal/agents.ts +248 -60
  66. package/assets/packages/cli/src/internal/commands.ts +82 -20
  67. package/assets/packages/cli/src/internal/documentation-bundle.ts +44 -13
  68. package/assets/packages/cli/src/internal/engine.ts +396 -87
  69. package/assets/packages/cli/src/internal/errors.ts +13 -5
  70. package/assets/packages/cli/src/internal/execution-context.ts +20 -12
  71. package/assets/packages/cli/src/internal/file-coordinator.ts +95 -26
  72. package/assets/packages/cli/src/internal/file-names.ts +4 -1
  73. package/assets/packages/cli/src/internal/launch-request.ts +74 -22
  74. package/assets/packages/cli/src/internal/logs.ts +10 -2
  75. package/assets/packages/cli/src/internal/metrics.ts +247 -61
  76. package/assets/packages/cli/src/internal/pi-assets.ts +105 -23
  77. package/assets/packages/cli/src/internal/run-lease.ts +72 -22
  78. package/assets/packages/cli/src/internal/run-log.ts +49 -20
  79. package/assets/packages/cli/src/internal/run-names.ts +120 -12
  80. package/assets/packages/cli/src/internal/run-state.ts +306 -71
  81. package/assets/packages/cli/src/internal/run-store.ts +216 -53
  82. package/assets/packages/cli/src/internal/usage.ts +23 -7
  83. package/assets/packages/cli/src/internal/worker-directory.ts +9 -3
  84. package/assets/packages/cli/src/internal/workflow-registry.ts +281 -70
  85. package/assets/packages/cli/src/internal/working-directory.ts +2 -1
  86. package/assets/packages/cli/src/workflow-loader.ts +341 -74
  87. package/assets/packages/core/src/atomic-files.ts +13 -4
  88. package/assets/packages/core/src/workflow-transition.ts +13 -2
  89. package/assets/packages/sdk/src/api.ts +248 -80
  90. package/assets/packages/sdk/src/index.ts +6 -1
  91. package/assets/packages/sdk/src/schema.ts +75 -21
  92. package/assets/tests/workflow-ref.test.ts +242 -57
  93. package/bin/norn.mjs +13 -4
  94. package/dist/cli.js +344 -90
  95. package/dist/client.js +100 -22
  96. package/dist/documentation.js +102 -25
  97. package/dist/generated-build-info.d.ts +2 -2
  98. package/dist/generated-build-info.js +2 -2
  99. package/dist/internal/agent-directory.js +3 -1
  100. package/dist/internal/agent-response-tool.js +42 -11
  101. package/dist/internal/agents.js +149 -37
  102. package/dist/internal/commands.js +65 -16
  103. package/dist/internal/documentation-bundle.js +23 -11
  104. package/dist/internal/engine.js +304 -66
  105. package/dist/internal/errors.js +4 -2
  106. package/dist/internal/execution-context.js +2 -1
  107. package/dist/internal/file-coordinator.js +59 -19
  108. package/dist/internal/launch-request.js +32 -14
  109. package/dist/internal/metrics.js +138 -38
  110. package/dist/internal/pi-assets.js +70 -19
  111. package/dist/internal/run-lease.js +29 -9
  112. package/dist/internal/run-log.js +24 -12
  113. package/dist/internal/run-state.js +172 -48
  114. package/dist/internal/run-store.js +157 -45
  115. package/dist/internal/worker-directory.js +6 -2
  116. package/dist/internal/workflow-registry.js +123 -32
  117. package/dist/internal/working-directory.js +2 -1
  118. package/dist/workflow-loader.js +198 -41
  119. 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> = { readonly id: string; readonly schema: Schema };
9
- export type SharedStateAccess = Pick<SharedState, "get" | "getOptional" | "set">;
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: { readonly path: string; readonly create: boolean }): Promise<SharedState> {
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("PRAGMA busy_timeout = 30000; PRAGMA journal_mode = DELETE;");
21
- if (input.create) database.exec("CREATE TABLE IF NOT EXISTS state (key TEXT PRIMARY KEY, value TEXT NOT NULL)");
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>(field: SharedStateField<Schema>): Promise<Static<Schema>> {
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) throw new Error(`Missing shared state: ${field.id}`);
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>(field: SharedStateField<Schema>): Promise<Static<Schema> | undefined> {
37
- const row = this.database.prepare("SELECT value FROM state WHERE key = ?").get(field.id);
38
- return row === undefined ? undefined : copyValue(field.schema, JSON.parse(String(row.value)));
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>(field: SharedStateField<Schema>, value: NoInfer<Static<Schema>>): Promise<void> {
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.prepare("INSERT INTO state (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(field.id, JSON.stringify(checked));
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>(schema: Schema, value: unknown): Static<Schema> {
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>(tool: ToolDefinition<Schema>): ToolDefinition<Schema> { return tool; }
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({ minimum: 0, description: "Zero-based UTF-16 offset into the serialized JSON. Start at 0." }),
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: { readonly state: SharedStateAccess; readonly fields: readonly StateFieldAccess[] }): ToolDefinition[] {
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) throw new Error("State tools require unique, explicitly selected fields");
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")) throw new Error(`State ${access} is not attached: ${key}`);
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: "List only attached workflow-state field IDs, permissions and value schemas. JSON is paginated; use nextOffset until null.",
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({ value: [...fields.values()].map(({ field, access }) => ({ id: field.id, access, schema: inspectSchema(field.schema) })), ...args });
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: "Read a selected workflow-state field. Unset fields return isSet:false. JSON is paginated; concurrent writes can change later pages, so compare revision before combining pages.",
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(selectField(args.key, "read"));
44
- return serializePage({ value: value === undefined ? { isSet: false } : { isSet: true, value }, ...args });
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: "Set an explicitly writable workflow-state field. Validate the value against its schema from norn_state_list. A get followed by set is not a transaction.",
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 { content: [{ type: "text", text: "Workflow state saved." }], details: {} };
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: { readonly value: unknown; readonly offset: number; readonly limit: number }) {
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 (!Number.isInteger(input.offset) || input.offset < 0 || !Number.isInteger(input.limit) || input.limit < 1 || input.limit > 10000) throw new Error("Invalid state output page");
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 = { text: serialized.slice(input.offset, end), nextOffset: end < serialized.length ? end : null, revision: createHash("sha256").update(serialized).digest("hex") };
67
- return { content: [{ type: "text" as const, text: JSON.stringify(details) }], details };
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 | 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. |
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 | 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. |
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
- "version": 1,
3
- "workflows": ["./plugin.ts"],
4
- "config": {
5
- "worktreeDevelopmentLoop": {
6
- "repositoryRoot": "/absolute/path/to/source-repository"
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 [developmentLoopWorkflow, planningWorkflow, implementationWorkflow, reviewWorkflow, reviewRouterWorkflow];
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({ name: "worktreeDevelopmentLoop", config: developmentLoopConfigSchema });
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(result: NornCommandRunResult): Promise<void> {
3
+ export async function ensureCommandSucceeded(
4
+ result: NornCommandRunResult,
5
+ ): Promise<void> {
4
6
  if (result.exitCode === 0) return;
5
- throw new Error(`${result.label} failed with exit code ${result.exitCode ?? "unknown"}: ${result.stderrTail || result.stdoutTail}`);
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: { instructions: "Use when you want a repository task planned, implemented, and reviewed in a separate clone with a caller approval gate. Starts from committed content and retains changes for inspection without automatically merging or pushing results back. Requires Git, Bash, and model access." },
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({ commands, paths, repositoryRoot: scope.config.repositoryRoot, baseRef: args.baseRef });
17
+ const repositoryPath = await materializeWorkspaceRepository({
18
+ commands,
19
+ paths,
20
+ repositoryRoot: scope.config.repositoryRoot,
21
+ baseRef: args.baseRef,
22
+ });
13
23
 
14
- return planningWorkflow({ task: args.task, repositoryPath, maxIterations: args.maxIterations });
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 { developmentLoopConfigSchema, developmentLoopArgsSchema } from "./schema.ts";
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({ commands, paths, repositoryRoot, baseRef }: { commands: NornCommands; paths: NornWorkflowPaths; repositoryRoot: string; baseRef: string }): Promise<string> {
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<typeof developmentLoopConfigSchema>;
14
- export type DevelopmentLoopArgs = StaticDecode<typeof developmentLoopArgsSchema>;
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({ args, paths, agents, commands, logs }): Promise<WorkflowResult> {
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 ? await readFile(join(paths.workspace, args.previousReviewPath), "utf8") : undefined;
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(args.task, plan, args.iteration, previousReview),
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(join(paths.workspace, `implementation/iteration-${args.iteration}-status.txt`), statusOutput);
34
- return reviewWorkflow({ ...args, implementationSummary: implementation.summary });
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(task: string, plan: string, iteration: number, previousReview: string | undefined): string {
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 ? "Address the previous review before making new changes." : undefined,
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
- ].filter((line): line is string => line !== undefined).join("\n");
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 { implementationAgentResponseSchema, implementationArgsSchema, type ImplementationArgs } from "./schema.ts";
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 {
@@ -1,2 +1,6 @@
1
1
  export { planningWorkflow } from "./execute.ts";
2
- export { planningAgentResponseSchema, planningArgsSchema, type PlanningArgs } from "./schema.ts";
2
+ export {
3
+ planningAgentResponseSchema,
4
+ planningArgsSchema,
5
+ type PlanningArgs,
6
+ } from "./schema.ts";
@@ -11,28 +11,46 @@ export const reviewWorkflow = developmentLoopScope.workflow({
11
11
  name: "review",
12
12
  entrypoint: false,
13
13
  args: reviewArgsSchema,
14
- async execute({ args, paths, agents, commands, logs }): Promise<WorkflowResult> {
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: "git status --short && git diff --stat HEAD -- . && git diff HEAD -- .",
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(join(paths.workspace, `review/iteration-${args.iteration}-diff.txt`), diffOutput);
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(args.task, plan, implementationSummary, diffOutput),
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(join(paths.workspace, automatedReviewPath), JSON.stringify(review, null, 2));
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(task: string, plan: string, implementationSummary: string, diff: string): string {
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
- "Use accept only when the work is ready. Use revise for fixable issues. Use blocked when manual input is needed. Include review details in the summary.",
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
-