@vimhead.dev/norn-cli 0.1.0-tip.35570530940.1 → 0.1.0-tip.35611592848.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.
@@ -14,7 +14,7 @@ Norn capabilities are ordinary TypeScript workflows: an agent can write one duri
14
14
  | Supply tools or tool wrappers to agents | [Custom tools](agents.md#custom-tools) | [Explicit shared state](../examples/shared-state/README.md) |
15
15
  | Persist application state or coordinate concurrent mutations | [Workflow-owned storage](persistence.md#workflow-owned-storage) | [Explicit shared state](../examples/shared-state/README.md), [Example-local work queue](../examples/coordinating-multiple-agents/README.md) |
16
16
  | Retain evidence or choose a filesystem boundary | [Persistence, files, and workspaces](persistence.md) | [Norn agent → saved file → analysis](../examples/agent-then-analysis/README.md) |
17
- | Reuse a workflow with a caller-selected continuation | [Composition](composition.md) | [Caller-selected continuation](../examples/caller-selected-continuation/README.md) |
17
+ | Reuse workflows with caller-selected continuations and routing policy | [Composition](composition.md) | [Caller-selected continuation](../examples/caller-selected-continuation/README.md), [Caller-owned routing](../examples/caller-owned-routing/README.md) |
18
18
  | Repair a failed run without repeating earlier work | [Recovery and gates](recovery.md) | [Analysis-only repair](../examples/agent-then-analysis/README.md#repair-only-the-analysis-step) |
19
19
 
20
20
  [Public types](../packages/sdk/src/api.ts) define the Norn SDK's authoring interface. CLI discovery exposes the currently loaded project, not a documentation-time workflow catalogue. See [installation](../README.md#installation) for runtime setup.
@@ -70,6 +70,21 @@ Contributions must be JSON objects matching the declared input type. Object-valu
70
70
 
71
71
  When passing a reference as input to another workflow, supply its JSON form shown above, not the function received in `args.next`.
72
72
 
73
+ ## Caller-owned routing policy
74
+
75
+ A continuation can be a caller-owned router rather than the final consumer. The
76
+ [caller-owned routing example](../examples/caller-owned-routing/README.md) combines
77
+ assessment and revision workflows: assessment reports findings, revision produces
78
+ an updated outline, and the router owns the acceptance threshold, next action,
79
+ and iteration limit. Neither reusable capability knows the caller's policy.
80
+
81
+ Task-level findings and run-level completion are separate: a caller may accept
82
+ some findings, request more work, or fail when its revision budget is exhausted.
83
+
84
+ | Decision | GOOD | BAD |
85
+ |---|---|---|
86
+ | IF callers need different acceptance or follow-up policies, THEN supply a caller-owned router as the continuation. ELSE use a direct continuation. | Route the same assessment to completion or revision using caller thresholds. | Embed one caller's revision budget in a reusable assessment, or add a router to unconditional delivery. |
87
+
73
88
  ## Multiple outcomes and direct targets
74
89
 
75
90
  References can be nested under ordinary author-selected names with independent contribution schemas:
@@ -4,6 +4,21 @@ The Norn SDK is the TypeScript interface for building reusable workflows. A work
4
4
 
5
5
  Start with the complete [minimal workflow](../examples/minimal-workflow/plugin.ts) and its [write/run/change exercise](../examples/minimal-workflow/README.md).
6
6
 
7
+ ## Develop agent workflows through execution
8
+
9
+ For agent-driven workflows, trial and error in native Norn runs is the default development method, starting with the first runnable step—not a final smoke test after implementation:
10
+
11
+ **Author → execute real agents → inspect saved evidence → repair → rollback and restore a valid checkpoint → resume → repeat.**
12
+
13
+ | Decision | GOOD | BAD |
14
+ |---|---|---|
15
+ | IF a workflow depends on agent behavior, THEN execute it early and frequently through Norn with bounded, representative inputs, using the loop above as the primary development process. ELSE deterministic workflows and helpers can be developed and validated with automated tests. | Exercise a real agent step before building the remaining orchestration; retain unit tests for scoring mathematics. | Build the whole agent workflow around mocked responses and postpone actual execution until the end. |
16
+ | IF inspecting an agent run, THEN compare retained outputs and evidence against the task requirements. ELSE do not claim the workflow works from status or test results alone. | Check a generated report against its source evidence and requested deliverable. | Treat `completed`, schema-valid JSON, or a passing deterministic suite as proof of task correctness. |
17
+ | IF repairing or iterating on an agent workflow, THEN use rollback, checkpoint restoration, and resume frequently to exercise the changed step while preserving valid earlier work. ELSE retain the inspected run as evidence for the unchanged behavior. | Restore the boundary after a valid assessment, repair delivery, and verify the assessment survives the resumed execution. | Restart every agent from scratch, simulate recovery only in tests, or assume restored files are correct without inspecting them. |
18
+ | IF credentials, inputs, access, or authorization block native execution, THEN report the blocker and mark agent behavior unvalidated. ELSE report the actual runs, inspected artifacts, and recovery exercised. | Distinguish passing deterministic checks from a blocked live agent run. | Substitute mocked success for execution or imply an unperformed recovery cycle passed. |
19
+
20
+ Use [source repair and rollback](recovery.md#source-repair-and-rollback) for checkpoint selection and external-effect precautions. The [agent → saved file → analysis repair exercise](../examples/agent-then-analysis/README.md#repair-only-the-analysis-step) demonstrates this loop with a real agent result retained across failure and recovery.
21
+
7
22
  ## Define a workflow
8
23
 
9
24
  `workflow` declares a complete, typed callable workflow:
@@ -0,0 +1,105 @@
1
+ # Caller-owned routing
2
+
3
+ Combine reusable assessment and revision workflows without making either own the
4
+ caller's acceptance threshold or iteration limit. This code-only example checks
5
+ Markdown outline headings, not prose quality. No model, credentials, local
6
+ dependencies, or compilation step is required.
7
+
8
+ Start with [caller-selected continuation](../caller-selected-continuation/README.md)
9
+ for reference inputs and forwarded arguments. Here, the supplied continuation is
10
+ a router rather than a delivery step:
11
+
12
+ ```text
13
+ assessOutline → routeAssessment → complete
14
+ │ → fail (revision limit)
15
+ ↓
16
+ appendHeading → assessOutline → routeAssessment → …
17
+ ```
18
+
19
+ ## Capability versus caller policy
20
+
21
+ - [assessment.ts](assessment.ts) reports which required headings are missing. It
22
+ does not decide whether those findings are acceptable or whether to revise.
23
+ - [revision.ts](revision.ts) appends one requested heading and contributes the
24
+ revised outline. It knows neither the assessment workflow nor the router.
25
+ - [router.ts](router.ts) owns acceptance, heading selection, the revision budget,
26
+ and the revision → reassessment → router chain. It saves the latest assessed
27
+ outline as workspace-relative `outline.md`, including on budget exhaustion.
28
+ - [contracts.ts](contracts.ts) declares the contribution schemas shared by callers
29
+ and capabilities; [norn.project.json](norn.project.json) registers all three workflows.
30
+ - [input.json](input.json) selects the router and captures its policy parameters
31
+ through `next.forwardArgs`, starting with `revisionsUsed: 0`.
32
+
33
+ The router accepts an assessment with at most `maxMissingHeadings` findings.
34
+ Otherwise it requests one revision, unless `maxRevisions` has been reached, in
35
+ which case the run fails. Acceptance is checked first: the final allowed revision
36
+ can still succeed. `maxRevisions` is bounded to 100 for this example.
37
+
38
+ Only the router imports both capabilities. Their `next` references could instead
39
+ select other registered consumers without changing either capability. The nested
40
+ references in the router are ordinary JSON inputs: revision contributes `outline`
41
+ to assessment; assessment contributes its findings back to the router. The
42
+ [composition reference](../../docs/composition.md) owns reference semantics.
43
+
44
+ ## Inspect and run
45
+
46
+ [Select the matching runtime](../../docs/cli.md#select-the-runtime), copy this
47
+ entire directory into a writable task directory, and `cd` into the copy.
48
+
49
+ ```bash
50
+ norn project inspect
51
+ norn workflows list --all
52
+ norn workflows inspect assessOutline
53
+ norn workflows inspect appendHeading
54
+ norn workflows inspect routeAssessment
55
+ norn runs start assessOutline < input.json
56
+ ```
57
+
58
+ Discovery should report `isComplete: true`. Copy the returned `run.id`:
59
+
60
+ ```bash
61
+ RUN=<returned-run-id>
62
+ norn runs wait "$RUN"
63
+ norn runs inspect "$RUN"
64
+ norn runs checkpoints "$RUN"
65
+ ```
66
+
67
+ Expected results:
68
+
69
+ - `run.status: completed`, `run.health: healthy`, and
70
+ `run.outcome.workflowId: routeAssessment`.
71
+ - Outcome metadata `data` contains `outlinePath: "outline.md"`,
72
+ `missingHeadings: []`, and `revisionsUsed: 2`.
73
+ - Transition checkpoints show assessment → router, then two repetitions of
74
+ router → revision → assessment → router.
75
+
76
+ Read `outline.md` in the inspected `run.paths.workspace`:
77
+
78
+ ```markdown
79
+ # Release notes
80
+
81
+ ## Summary
82
+
83
+ ## Changes
84
+
85
+ ## Verification
86
+ ```
87
+
88
+ These are empty sections, not a completed release note. The example's contract is
89
+ heading presence only; an actual content assessment needs a different capability.
90
+
91
+ ## Change only the caller's policy
92
+
93
+ For each case, edit `args.next.forwardArgs` in `input.json`, start a **new** run,
94
+ and inspect that run and its `outline.md`. Leave both capability modules unchanged.
95
+
96
+ | `maxMissingHeadings` | `maxRevisions` | Expected result |
97
+ |---|---|---|
98
+ | `2` | `0` | Completes immediately with two missing headings and `revisionsUsed: 0`; no revision transition. |
99
+ | `1` | `2` | Completes after one revision, with only `Verification` missing. |
100
+ | `0` | `1` | Fails in the router after one revision, with `Verification` still missing; retained outline includes `Changes`. |
101
+ | `0` | `0` | Fails immediately with both headings missing; no revision transition. |
102
+
103
+ Keep `revisionsUsed: 0` for each fresh run. In the failure cases, the router reports
104
+ that the revision limit was reached rather than completing below the requested
105
+ threshold. A successful `runs wait` command alone does not imply workflow success.
@@ -0,0 +1,22 @@
1
+ import { workflow, workflowRefSchema } from "@vimhead.dev/norn";
2
+ import { Type } from "typebox";
3
+ import { assessmentContributionSchema, headingsSchema, outlineContributionSchema } from "./contracts.ts";
4
+
5
+ export const assessOutline = workflow({
6
+ name: "assessOutline",
7
+ entrypoint: {
8
+ instructions: "Check an outline for exact, case-sensitive '## heading' lines from requiredHeadings. Pass outline, requiredHeadings, and missingHeadings to the caller-selected next workflow. Reports heading presence only, not content quality; no model or external service is used.",
9
+ },
10
+ args: Type.Object({
11
+ ...outlineContributionSchema.properties,
12
+ requiredHeadings: headingsSchema,
13
+ next: workflowRefSchema({ args: assessmentContributionSchema }),
14
+ }),
15
+ execute({ args }) {
16
+ const lines = new Set(args.outline.split(/\r?\n/));
17
+ const missingHeadings = args.requiredHeadings.filter(heading => !lines.has(`## ${heading}`));
18
+ return args.next({ outline: args.outline, requiredHeadings: args.requiredHeadings, missingHeadings });
19
+ },
20
+ });
21
+
22
+ export default [assessOutline];
@@ -0,0 +1,10 @@
1
+ import { Type } from "typebox";
2
+
3
+ export const headingSchema = Type.String({ minLength: 1, pattern: "^[^\\r\\n]+$" });
4
+ export const headingsSchema = Type.Array(headingSchema, { uniqueItems: true });
5
+ export const outlineContributionSchema = Type.Object({ outline: Type.String() });
6
+ export const assessmentContributionSchema = Type.Object({
7
+ ...outlineContributionSchema.properties,
8
+ requiredHeadings: headingsSchema,
9
+ missingHeadings: headingsSchema,
10
+ });
@@ -0,0 +1,14 @@
1
+ {
2
+ "args": {
3
+ "outline": "# Release notes\n\n## Summary\n",
4
+ "requiredHeadings": ["Summary", "Changes", "Verification"],
5
+ "next": {
6
+ "workflow": "routeAssessment",
7
+ "forwardArgs": {
8
+ "maxMissingHeadings": 0,
9
+ "maxRevisions": 2,
10
+ "revisionsUsed": 0
11
+ }
12
+ }
13
+ }
14
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "version": 1,
3
+ "workflows": ["./assessment.ts", "./revision.ts", "./router.ts"]
4
+ }
@@ -0,0 +1,20 @@
1
+ import { workflow, workflowRefSchema } from "@vimhead.dev/norn";
2
+ import { Type } from "typebox";
3
+ import { headingSchema, outlineContributionSchema } from "./contracts.ts";
4
+
5
+ export const appendHeading = workflow({
6
+ name: "appendHeading",
7
+ entrypoint: {
8
+ instructions: "Append one empty level-two Markdown section named heading to outline, then pass the revised outline to the caller-selected next workflow. Does not assess the outline or select further work; no model or external service is used.",
9
+ },
10
+ args: Type.Object({
11
+ ...outlineContributionSchema.properties,
12
+ heading: headingSchema,
13
+ next: workflowRefSchema({ args: outlineContributionSchema }),
14
+ }),
15
+ execute({ args }) {
16
+ return args.next({ outline: `${args.outline.trimEnd()}\n\n## ${args.heading}\n` });
17
+ },
18
+ });
19
+
20
+ export default [appendHeading];
@@ -0,0 +1,49 @@
1
+ import { writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { workflow, type WorkflowResult } from "@vimhead.dev/norn";
4
+ import { Type } from "typebox";
5
+ import { assessOutline } from "./assessment.ts";
6
+ import { assessmentContributionSchema } from "./contracts.ts";
7
+ import { appendHeading } from "./revision.ts";
8
+
9
+ export const routeAssessment = workflow({
10
+ name: "routeAssessment",
11
+ entrypoint: false,
12
+ args: Type.Object({
13
+ ...assessmentContributionSchema.properties,
14
+ maxMissingHeadings: Type.Integer({ minimum: 0 }),
15
+ maxRevisions: Type.Integer({ minimum: 0, maximum: 100 }),
16
+ revisionsUsed: Type.Integer({ minimum: 0, maximum: 100 }),
17
+ }),
18
+ async execute({ args, paths, run }): Promise<WorkflowResult> {
19
+ const outlinePath = "outline.md";
20
+ await writeFile(join(paths.workspace, outlinePath), args.outline);
21
+ const data = { outlinePath, missingHeadings: args.missingHeadings, revisionsUsed: args.revisionsUsed };
22
+ if (args.missingHeadings.length <= args.maxMissingHeadings) {
23
+ return run.complete({ summary: "Outline meets the caller's heading threshold.", data });
24
+ }
25
+ if (args.revisionsUsed >= args.maxRevisions) {
26
+ return run.fail({ summary: "Revision limit reached before the outline met the caller's heading threshold.", data });
27
+ }
28
+ return appendHeading({
29
+ outline: args.outline,
30
+ heading: args.missingHeadings[0],
31
+ next: {
32
+ workflow: assessOutline.id,
33
+ forwardArgs: {
34
+ requiredHeadings: args.requiredHeadings,
35
+ next: {
36
+ workflow: routeAssessment.id,
37
+ forwardArgs: {
38
+ maxMissingHeadings: args.maxMissingHeadings,
39
+ maxRevisions: args.maxRevisions,
40
+ revisionsUsed: args.revisionsUsed + 1,
41
+ },
42
+ },
43
+ },
44
+ },
45
+ });
46
+ },
47
+ });
48
+
49
+ export default [routeAssessment];
@@ -1 +1 @@
1
- {"version":"0.1.0-tip.35570530940.1"}
1
+ {"version":"0.1.0-tip.35611592848.1"}
@@ -2,8 +2,8 @@ import type { NornBuildInfo } from "./build-info.ts";
2
2
 
3
3
  export const NORN_GENERATED_BUILD_INFO = {
4
4
  "kind": "npm-registry",
5
- "version": "0.1.0-tip.35570530940.1",
6
- "commit": "48ecaf47ae0e82e17970db9465497a4e1ad5ff10",
5
+ "version": "0.1.0-tip.35611592848.1",
6
+ "commit": "ece526887ef7a821f5e08062d1553e8a5273bd86",
7
7
  "packageSpec": "@vimhead.dev/norn-cli@tip",
8
8
  "upgrade": {
9
9
  "supported": false,
@@ -1,7 +1,7 @@
1
1
  export declare const NORN_GENERATED_BUILD_INFO: {
2
2
  readonly kind: "npm-registry";
3
- readonly version: "0.1.0-tip.35570530940.1";
4
- readonly commit: "48ecaf47ae0e82e17970db9465497a4e1ad5ff10";
3
+ readonly version: "0.1.0-tip.35611592848.1";
4
+ readonly commit: "ece526887ef7a821f5e08062d1553e8a5273bd86";
5
5
  readonly packageSpec: "@vimhead.dev/norn-cli@tip";
6
6
  readonly upgrade: {
7
7
  readonly supported: false;
@@ -1,8 +1,8 @@
1
1
  // src/generated-build-info.ts
2
2
  var NORN_GENERATED_BUILD_INFO = {
3
3
  "kind": "npm-registry",
4
- "version": "0.1.0-tip.35570530940.1",
5
- "commit": "48ecaf47ae0e82e17970db9465497a4e1ad5ff10",
4
+ "version": "0.1.0-tip.35611592848.1",
5
+ "commit": "ece526887ef7a821f5e08062d1553e8a5273bd86",
6
6
  "packageSpec": "@vimhead.dev/norn-cli@tip",
7
7
  "upgrade": {
8
8
  "supported": false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vimhead.dev/norn-cli",
3
- "version": "0.1.0-tip.35570530940.1",
3
+ "version": "0.1.0-tip.35611592848.1",
4
4
  "description": "Harness-agnostic runtime for agent-driven and code-driven workflows, built primarily for agents.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -43,7 +43,7 @@
43
43
  "@earendil-works/pi-coding-agent": "0.85.1",
44
44
  "jiti": "^2.7.0",
45
45
  "typebox": "^1.3.34",
46
- "@vimhead.dev/norn": "0.1.0-tip.35570530940.1"
46
+ "@vimhead.dev/norn": "0.1.0-tip.35611592848.1"
47
47
  },
48
48
  "scripts": {
49
49
  "build": "node ../../scripts/build-package.ts"