@vimhead.dev/norn-cli 0.1.0-tip.35240723931.1 → 0.1.0-tip.35343816255.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 +2 -24
- package/assets/docs/README.md +3 -1
- package/assets/docs/agents.md +3 -3
- package/assets/docs/cli.md +7 -7
- package/assets/docs/composition.md +64 -34
- package/assets/docs/persistence.md +4 -2
- package/assets/docs/projects.md +3 -12
- package/assets/{setup → docs}/providers.md +4 -3
- package/assets/docs/resources.md +7 -7
- package/assets/docs/schemas.md +130 -0
- package/assets/docs/workflows.md +4 -3
- package/assets/examples/agent-then-analysis/README.md +1 -1
- package/assets/examples/agent-then-analysis/plugin.ts +16 -15
- package/assets/examples/caller-selected-continuation/README.md +75 -0
- package/assets/examples/caller-selected-continuation/caller.ts +47 -0
- package/assets/examples/caller-selected-continuation/input.json +9 -0
- package/assets/examples/caller-selected-continuation/norn.project.json +4 -0
- package/assets/examples/caller-selected-continuation/producer.ts +32 -0
- package/assets/examples/coordinating-multiple-agents/README.md +1 -1
- package/assets/examples/coordinating-multiple-agents/plugin.ts +10 -11
- package/assets/examples/coordinating-multiple-agents/queue-adapter.ts +3 -4
- package/assets/examples/coordinating-multiple-agents/work-queue.ts +18 -18
- package/assets/examples/minimal-workflow/plugin.ts +2 -2
- package/assets/examples/shared-state/README.md +1 -1
- package/assets/examples/shared-state/plugin.ts +6 -6
- package/assets/examples/worktree-development-loop/README.md +105 -44
- package/assets/examples/worktree-development-loop/norn.project.json +1 -1
- package/assets/examples/worktree-development-loop/state.ts +6 -6
- package/assets/examples/worktree-development-loop/workflows/development-loop/execute.ts +1 -1
- package/assets/examples/worktree-development-loop/workflows/development-loop/schema.ts +9 -9
- package/assets/examples/worktree-development-loop/workflows/implementation/execute.ts +1 -1
- package/assets/examples/worktree-development-loop/workflows/implementation/schema.ts +7 -7
- package/assets/examples/worktree-development-loop/workflows/planning/execute.ts +1 -1
- package/assets/examples/worktree-development-loop/workflows/planning/schema.ts +7 -7
- package/assets/examples/worktree-development-loop/workflows/review/execute.ts +1 -1
- package/assets/examples/worktree-development-loop/workflows/review/schema.ts +11 -11
- package/assets/examples/worktree-development-loop/workflows/review-router/execute.ts +1 -1
- package/assets/examples/worktree-development-loop/workflows/review-router/schema.ts +5 -5
- package/assets/package.json +1 -1
- package/assets/packages/cli/src/cli.ts +15 -32
- package/assets/packages/cli/src/client.ts +0 -7
- package/assets/packages/cli/src/generated-build-info.ts +2 -2
- package/assets/packages/cli/src/internal/agent-response-tool.ts +9 -10
- package/assets/packages/cli/src/internal/agents.ts +17 -16
- package/assets/packages/cli/src/internal/engine.ts +20 -18
- package/assets/packages/cli/src/internal/errors.ts +4 -10
- package/assets/packages/cli/src/internal/run-state.ts +4 -0
- package/assets/packages/cli/src/internal/run.ts +4 -7
- package/assets/packages/cli/src/internal/state-store.ts +22 -14
- package/assets/packages/cli/src/internal/workflow-registry.ts +27 -21
- package/assets/packages/cli/src/plugin-loader.ts +33 -45
- package/assets/packages/cli/src/resources.ts +14 -12
- package/assets/packages/core/src/workflow-transition.ts +4 -0
- package/assets/packages/sdk/src/api.ts +87 -131
- package/assets/packages/sdk/src/files.ts +11 -10
- package/assets/packages/sdk/src/index.ts +0 -1
- package/assets/packages/sdk/src/resources.ts +2 -2
- package/assets/packages/sdk/src/schema.ts +53 -29
- package/assets/packages/sdk/src/state-adapter.ts +4 -4
- package/assets/tests/workflow-ref.test.ts +121 -77
- package/dist/cli.js +20 -36
- package/dist/client.d.ts +0 -4
- package/dist/client.js +0 -3
- package/dist/generated-build-info.d.ts +2 -2
- package/dist/generated-build-info.js +2 -2
- package/dist/internal/agent-response-tool.d.ts +2 -2
- package/dist/internal/agent-response-tool.js +7 -7
- package/dist/internal/agents.d.ts +2 -2
- package/dist/internal/agents.js +10 -10
- package/dist/internal/engine.d.ts +1 -1
- package/dist/internal/engine.js +14 -12
- package/dist/internal/errors.d.ts +2 -1
- package/dist/internal/errors.js +5 -8
- package/dist/internal/run-state.js +4 -0
- package/dist/internal/run.d.ts +2 -2
- package/dist/internal/run.js +10 -6
- package/dist/internal/state-store.d.ts +8 -7
- package/dist/internal/state-store.js +15 -9
- package/dist/internal/workflow-registry.d.ts +14 -6
- package/dist/internal/workflow-registry.js +20 -15
- package/dist/plugin-loader.d.ts +15 -21
- package/dist/plugin-loader.js +35 -47
- package/dist/resources.d.ts +1 -1
- package/dist/resources.js +15 -13
- package/package.json +3 -4
- package/assets/packages/sdk/src/seer/config.ts +0 -62
- package/assets/packages/sdk/src/seer/index.ts +0 -7
- package/assets/setup/releases.md +0 -76
package/assets/README.md
CHANGED
|
@@ -63,7 +63,7 @@ Inside the interactive session:
|
|
|
63
63
|
|
|
64
64
|
Model-free workflows need no provider authentication. For other credential
|
|
65
65
|
methods, custom providers, and Norn's configuration directory, see
|
|
66
|
-
[providers and authentication](
|
|
66
|
+
[providers and authentication](docs/providers.md).
|
|
67
67
|
|
|
68
68
|
### 3. Optionally connect your harness
|
|
69
69
|
|
|
@@ -104,11 +104,6 @@ NORN_EXECUTABLE=/absolute/path/to/norn cursor .
|
|
|
104
104
|
Need an adapter for another harness? [Open an issue](https://github.com/vimhead/norn/issues/new)
|
|
105
105
|
with the harness name.
|
|
106
106
|
|
|
107
|
-
## Setting up a Norn project
|
|
108
|
-
|
|
109
|
-
[Projects and loading](docs/projects.md) covers initialization, registration,
|
|
110
|
-
reusable configuration, dependencies, source reload, and discovery diagnostics.
|
|
111
|
-
|
|
112
107
|
## Build workflows with the Norn SDK
|
|
113
108
|
|
|
114
109
|
Install SDK types and helpers for TypeScript/editor support:
|
|
@@ -121,22 +116,6 @@ Use the SDK version reported by `norn version` for an exact runtime match. Runti
|
|
|
121
116
|
execution also supplies [virtual SDK imports](docs/projects.md#import-and-reload),
|
|
122
117
|
so standalone examples need no local SDK installation.
|
|
123
118
|
|
|
124
|
-
[Workflow authoring](docs/workflows.md) covers declarations, implementations,
|
|
125
|
-
commands, and run outcomes. Focused companion references:
|
|
126
|
-
|
|
127
|
-
- [Norn agents](docs/agents.md)
|
|
128
|
-
- [State, artifacts, and workspaces](docs/persistence.md)
|
|
129
|
-
- [Composition and reuse](docs/composition.md)
|
|
130
|
-
- [Recovery and gates](docs/recovery.md)
|
|
131
|
-
|
|
132
|
-
The larger [worktree development loop](examples/worktree-development-loop/README.md)
|
|
133
|
-
is an optional composition example, not a required workflow architecture.
|
|
134
|
-
|
|
135
|
-
## Run workflows with Norn
|
|
136
|
-
|
|
137
|
-
[CLI and client](docs/cli.md) covers live contract discovery, JSON invocation,
|
|
138
|
-
launch/wait semantics, results, and use from other harnesses.
|
|
139
|
-
|
|
140
119
|
## Development
|
|
141
120
|
|
|
142
121
|
```bash
|
|
@@ -153,5 +132,4 @@ published dependency. No separate core build is needed.
|
|
|
153
132
|
|
|
154
133
|
Checks cover package builds, TypeScript (including examples, adapters, scripts,
|
|
155
134
|
and tests), runtime regressions, isolated npm installations, and standalone binaries.
|
|
156
|
-
`pack:dry` creates local tarballs without publishing.
|
|
157
|
-
[release setup](setup/releases.md) for bootstrap and trusted publishing.
|
|
135
|
+
`pack:dry` creates local tarballs without publishing.
|
package/assets/docs/README.md
CHANGED
|
@@ -7,12 +7,14 @@ Norn capabilities are ordinary TypeScript plugins: an agent can write one during
|
|
|
7
7
|
| Task | Documentation | Runnable example |
|
|
8
8
|
|---|---|---|
|
|
9
9
|
| Build, register, or diagnose workflows with the Norn SDK | [Projects and loading](projects.md), [Workflow authoring](workflows.md) | [Create → run → change](../examples/minimal-workflow/README.md) |
|
|
10
|
+
| Define TypeBox schemas, constraints, or codecs | [TypeBox schemas](schemas.md) | — |
|
|
10
11
|
| Discover contracts or invoke Norn from another harness | [CLI and client](cli.md) | [Create → run → change](../examples/minimal-workflow/README.md) |
|
|
12
|
+
| Configure providers, models, and authentication for Norn agents | [Providers and authentication](providers.md) | — |
|
|
11
13
|
| Delegate work with explicit inputs and structured results | [Norn agents](agents.md) | [Norn agent → saved artifact → analysis](../examples/agent-then-analysis/README.md) |
|
|
12
14
|
| Initialize shared resources, attach state tools, or coordinate file mutations | [Resources and locking](resources.md) | [Explicit shared state](../examples/shared-state/README.md) |
|
|
13
15
|
| Implement a custom resource and adapter to coordinate concurrent agents | [Resource contracts](resources.md) | [Example-local work queue](../examples/coordinating-multiple-agents/README.md) |
|
|
14
16
|
| Retain evidence or choose a filesystem boundary | [State, artifacts, and workspaces](persistence.md) | [Norn agent → saved artifact → analysis](../examples/agent-then-analysis/README.md) |
|
|
15
|
-
| Reuse a workflow with a caller-selected continuation | [Composition](composition.md) | [
|
|
17
|
+
| Reuse a workflow with a caller-selected continuation | [Composition](composition.md) | [Caller-selected continuation](../examples/caller-selected-continuation/README.md) |
|
|
16
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) |
|
|
17
19
|
|
|
18
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.
|
package/assets/docs/agents.md
CHANGED
|
@@ -6,7 +6,7 @@ The [Norn agent → saved artifact → analysis example](../examples/agent-then-
|
|
|
6
6
|
|
|
7
7
|
## One prompt or a retained session
|
|
8
8
|
|
|
9
|
-
`run.agents.prompt({ label, prompt, response, ...sessionOptions })` creates a Pi session
|
|
9
|
+
`run.agents.prompt({ label, prompt, response, ...sessionOptions })` creates a Pi session for one prompt and returns the value described by the response schema, including any codec transformations—not `{ response, raw }`. You do not need to dispose this one-prompt session. See [schema input/output types](schemas.md#codecs-and-inputoutput-types).
|
|
10
10
|
|
|
11
11
|
For follow-up turns in the same conversation:
|
|
12
12
|
|
|
@@ -34,7 +34,7 @@ try {
|
|
|
34
34
|
}
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
`task` and both
|
|
37
|
+
`task` and both TypeBox schemas are capability-specific inputs in this fragment. [Provider setup](providers.md) is a prerequisite for Norn agents; detached execution cannot conduct an interactive login.
|
|
38
38
|
|
|
39
39
|
## Response contract and evidence
|
|
40
40
|
|
|
@@ -52,7 +52,7 @@ Successful results and raw attempts are written under `current/logs/agents/`; Pi
|
|
|
52
52
|
|
|
53
53
|
The default tool allowlist is `read`, `bash`, `edit`, `write`, plus the response tool. An explicit `tools: []` requests no built-in task tools, but still includes the response tool and any explicitly attached [resource-adapter tools](resources.md#explicit-agent-attachment). `resourceAdapters` is accepted by both session creation and one-shot prompting; omitting it attaches no workflow state.
|
|
54
54
|
|
|
55
|
-
Each session
|
|
55
|
+
Each session loads resources for its `cwd` and [Norn configuration](providers.md#norn-configuration). Installed provider extensions register before default-model selection. Both `run.agents.createSession` and `run.agents.prompt` accept per-session `model` and `thinkingLevel` overrides; omitted values use Pi's configured selection and defaults. Discoverable settings, skills, context files, and extensions can therefore affect it. It does **not** inherit the outer conversation or its in-memory tool registrations. Loaded extensions may change active tools; the requested tool list alone is not an adversarial restriction.
|
|
56
56
|
|
|
57
57
|
`systemPrompt` replaces the base prompt; `appendSystemPrompt` adds to resource-loader append content. Pi's default self-documentation block is absent with a custom base prompt. Context files and applicable skill advertisements can still be appended by Pi. Norn currently does not automatically inject a Norn authoring bootstrap.
|
|
58
58
|
|
package/assets/docs/cli.md
CHANGED
|
@@ -45,7 +45,7 @@ npm/source installations resolve their built `assets/` tree directly from the CL
|
|
|
45
45
|
package, not the current directory or another executable on PATH. A version mismatch
|
|
46
46
|
fails rather than advertising another build's documentation. Compiled binaries embed the docs,
|
|
47
47
|
examples, and source references, preserving relative links. The first
|
|
48
|
-
inspection
|
|
48
|
+
inspection makes a complete copy available in a build/content-specific
|
|
49
49
|
cache; later calls verify and reuse it without rewriting files. Different asset
|
|
50
50
|
contents or build commits use separate entries. `version` and help do not extract
|
|
51
51
|
anything. No prompt augmentation or agent-context delivery is performed.
|
|
@@ -57,9 +57,9 @@ Cache roots:
|
|
|
57
57
|
- Windows: `%LOCALAPPDATA%/norn/docs/`, or `~/AppData/Local/norn/docs/`
|
|
58
58
|
- Explicit override: `NORN_DOCS_CACHE_DIR`
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
Existing cache entries must match the bundled files exactly, with no missing or
|
|
61
|
+
extra files or symlinks. Corrupt entries are not silently overwritten; the error
|
|
62
|
+
names the entry to remove before retrying.
|
|
63
63
|
This is accidental-corruption detection, not a sandbox against another process
|
|
64
64
|
with access to the same user's cache. Old build entries are not automatically
|
|
65
65
|
removed. The extracted tree is a documentation snapshot, not a separate runtime
|
|
@@ -82,7 +82,7 @@ norn docs intro
|
|
|
82
82
|
Returns `{ "intro": "..." }`: a compact authoring introduction with runtime
|
|
83
83
|
version/commit, invocation, and pointers to the documentation index and examples.
|
|
84
84
|
Topic routing remains in the index; the command does not copy manuals or enumerate
|
|
85
|
-
workflows. It
|
|
85
|
+
workflows. It returns locations for the same installed documentation as `docs inspect`
|
|
86
86
|
and works without a valid project.
|
|
87
87
|
|
|
88
88
|
The invocation is a JSON argument array, not a shell command string. Its first
|
|
@@ -113,7 +113,7 @@ Default workflow listing shows entrypoints; `--all` includes internal steps. Wor
|
|
|
113
113
|
Help is text; ordinary results are JSON. `runs logs` emits JSONL events.
|
|
114
114
|
`norn pi [arguments...]` is a passthrough to bundled Pi, preserving Pi's native
|
|
115
115
|
output and exit status rather than wrapping them in Norn JSON. Use `norn pi --help`
|
|
116
|
-
for Pi's options and [provider setup](
|
|
116
|
+
for Pi's options and [provider setup](providers.md) for operator instructions.
|
|
117
117
|
Commands and schemas from the invoked executable are authoritative when a checkout and installation differ.
|
|
118
118
|
|
|
119
119
|
## Start, wait, inspect
|
|
@@ -178,6 +178,6 @@ console.log(finished.outcome?.metadata);
|
|
|
178
178
|
|
|
179
179
|
The client defaults to its own package's `bin/norn.mjs`. Its optional `executablePath` is a script launched through `process.execPath`, not an arbitrary standalone binary or shell command. Other languages can invoke the CLI directly with cwd, JSON stdin, and parsed stdout.
|
|
180
180
|
|
|
181
|
-
`workflows.list()` and `inspect()` use fresh CLI discovery. `workflows.entries()` and `client.state` use a cached in-process project load;
|
|
181
|
+
`workflows.list()` and `inspect()` use fresh CLI discovery. `workflows.entries()` and `client.state` use a cached in-process project load; [persistence](persistence.md#choose-what-survives) defines the `client.state` lifetime. A new client is needed to refresh that in-process catalogue after source edits.
|
|
182
182
|
|
|
183
183
|
Sources: [CLI declarations and handlers](../packages/cli/src/cli.ts), [client API](../packages/cli/src/client.ts).
|
|
@@ -3,45 +3,42 @@
|
|
|
3
3
|
## Transfer, not a returning call
|
|
4
4
|
|
|
5
5
|
```ts
|
|
6
|
-
return
|
|
6
|
+
return manifest.workflows.analyze({ draftArtifact });
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Return a workflow call to select the next step in the same run. Supply its complete input; TypeScript checks it against the declaration's params schema. The target must be registered in the loaded project.
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
This transfers control rather than calling a subroutine: awaiting the declaration does not execute the target or return its eventual result. Steps share run state and artifacts, not local variables or agent conversations. `run.complete` completes the whole run.
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
For a dynamically selected string ID, use `return run.next(workflowId, params)`. The selected target checks its input at runtime. `run.next` accepts IDs, not declarations or reference functions.
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
## Caller-selected workflow reference
|
|
16
|
+
|
|
17
|
+
The [caller-selected continuation example](../examples/caller-selected-continuation/README.md)
|
|
18
|
+
runs a producer with either of two caller-selected consumers, forwarding caller
|
|
19
|
+
context alongside the producer's results. It needs no model or credentials.
|
|
20
|
+
|
|
21
|
+
A reusable capability can accept a workflow reference whose schema describes the values it contributes. The caller supplies the target and captures the remaining params:
|
|
16
22
|
|
|
17
23
|
```ts
|
|
18
24
|
import { artifactRefSchema, workflowRefSchema } from "@vimhead.dev/norn";
|
|
19
|
-
import {
|
|
20
|
-
|
|
21
|
-
const paramsSchema =
|
|
22
|
-
task:
|
|
23
|
-
next:
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
import { Type } from "typebox";
|
|
26
|
+
|
|
27
|
+
const paramsSchema = Type.Object({
|
|
28
|
+
task: Type.String(),
|
|
29
|
+
next: Type.Union([
|
|
30
|
+
workflowRefSchema({
|
|
31
|
+
params: Type.Object({
|
|
32
|
+
resultArtifact: artifactRefSchema,
|
|
33
|
+
summary: Type.String(),
|
|
34
|
+
}),
|
|
27
35
|
}),
|
|
28
|
-
|
|
36
|
+
Type.Null(),
|
|
37
|
+
]),
|
|
29
38
|
});
|
|
30
39
|
```
|
|
31
40
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
```ts
|
|
35
|
-
return params.next
|
|
36
|
-
? run.next(params.next.workflow, {
|
|
37
|
-
...params.next.forwardParams,
|
|
38
|
-
resultArtifact,
|
|
39
|
-
summary,
|
|
40
|
-
})
|
|
41
|
-
: run.complete({ summary, artifacts: { result: resultArtifact } });
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
A caller supplies a registered target and its own context:
|
|
41
|
+
A caller supplies the target and its own parameters:
|
|
45
42
|
|
|
46
43
|
```json
|
|
47
44
|
{
|
|
@@ -55,22 +52,55 @@ A caller supplies a registered target and its own context:
|
|
|
55
52
|
}
|
|
56
53
|
```
|
|
57
54
|
|
|
58
|
-
|
|
55
|
+
Code uses a declaration's `.id` in the reference payload, not the declaration itself. A bare ID string is shorthand for an object reference with empty `forwardParams`.
|
|
56
|
+
|
|
57
|
+
Inside the workflow, `params.next` is a function. Supply only the result fields declared above:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
return params.next
|
|
61
|
+
? params.next({ resultArtifact, summary })
|
|
62
|
+
: run.complete({ summary, artifacts: { result: resultArtifact } });
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`importer.deliver` receives `batchId` from the caller plus `resultArtifact` and `summary` from the producer. Its params schema must accept all three. Produced fields replace caller fields with the same name; nested objects are replaced, not deep-merged.
|
|
66
|
+
|
|
67
|
+
Contributions must be JSON objects matching the declared input type. Object-valued records, unions, intersections and codecs are supported. With codecs, supply `StaticEncode` values, just as for a direct workflow call—not transformed `StaticDecode` values.
|
|
68
|
+
|
|
69
|
+
`workflows inspect` shows the contribution contract under `x-norn-workflow-ref.contributedParamsSchema`. Use it alongside the target's params schema to check that the combined input fits.
|
|
70
|
+
|
|
71
|
+
When passing a reference as input to another workflow, supply its JSON form shown above, not the function received in `params.next`.
|
|
72
|
+
|
|
73
|
+
## Multiple outcomes and direct targets
|
|
74
|
+
|
|
75
|
+
References can be nested under ordinary author-selected names with independent contribution schemas:
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
const next = Type.Object({
|
|
79
|
+
success: workflowRefSchema({
|
|
80
|
+
params: Type.Object({ resultArtifact: artifactRefSchema }),
|
|
81
|
+
}),
|
|
82
|
+
failure: workflowRefSchema({
|
|
83
|
+
params: Type.Object({ reason: Type.String() }),
|
|
84
|
+
}),
|
|
85
|
+
});
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
An implementation can return `params.next.success({ resultArtifact })`, `params.next.failure({ reason })`, or select its own known target with `manualReviewWorkflow({ task, reason })`. A dynamic target still uses `run.next(selectedId, input)`.
|
|
59
89
|
|
|
60
|
-
|
|
90
|
+
These are alternative transitions, not fan-out. `success` and `failure` are not reserved names, and a failure reference does not catch unhandled exceptions automatically.
|
|
61
91
|
|
|
62
92
|
| Decision | GOOD | BAD |
|
|
63
93
|
|---|---|---|
|
|
64
|
-
| IF
|
|
65
|
-
| IF
|
|
66
|
-
| IF a target schema changes, THEN
|
|
94
|
+
| IF the caller selects the next step, THEN invoke its reference with the declared contribution. ELSE call a known declaration or use a dynamic ID. | `params.next({ resultArtifact })` | Manually reconstruct captured forwarding input. |
|
|
95
|
+
| IF additional caller work follows the result, THEN represent it as the supplied reference. ELSE complete the run. | `assess → caller.deliver` | Expect execution to return to the line following a workflow call. |
|
|
96
|
+
| IF a target schema changes, THEN exercise the assembled input contract. ELSE preserve its existing input contract. | Verify the target accepts captured context and contributed results. | Treat contribution metadata as end-to-end compatibility proof. |
|
|
67
97
|
|
|
68
|
-
The [worktree development loop](../examples/worktree-development-loop/README.md) is a larger
|
|
98
|
+
The [worktree development loop](../examples/worktree-development-loop/README.md) is a larger multi-step example using known workflow declarations, not a required planner/reviewer architecture.
|
|
69
99
|
|
|
70
100
|
## Another project or harness
|
|
71
101
|
|
|
72
102
|
Reuse source by explicitly registering it, directly or through [included config](projects.md). There is no required package layout. Plugin IDs must remain unique within a project; run state and artifacts belong to the invoking project/run, not the plugin source directory.
|
|
73
103
|
|
|
74
|
-
An external shell, Python program, or agent harness can call the [CLI](cli.md) from the target project directory. The JavaScript client offers the same lifecycle without inventing another orchestration layer. Separate CLI starts create separate runs; connecting their artifact content is a caller responsibility, unlike same-run
|
|
104
|
+
An external shell, Python program, or agent harness can call the [CLI](cli.md) from the target project directory. The JavaScript client offers the same lifecycle without inventing another orchestration layer. Separate CLI starts create separate runs; connecting their artifact content is a caller responsibility, unlike same-run references.
|
|
75
105
|
|
|
76
106
|
Sources: [reference schemas and controls](../packages/sdk/src/api.ts), [scheduler](../packages/cli/src/internal/engine.ts), [reference contract tests](../tests/workflow-ref.test.ts).
|
|
@@ -4,13 +4,15 @@
|
|
|
4
4
|
|
|
5
5
|
| Value | Lifetime and access |
|
|
6
6
|
|---|---|
|
|
7
|
-
| Local variables / plugin factory memory | Current executor only; not a resume contract.
|
|
7
|
+
| Local variables / plugin factory memory | Current invocation or executor only; not a resume contract. |
|
|
8
|
+
| Factory context `state` / `client.state` | In-memory registration state for the loaded project, not a selected run's persisted state. |
|
|
8
9
|
| `run.state` | Per-run, schema-validated JSON shared across steps. `get` requires a value; `getOptional` permits absence; `set` persists it. |
|
|
10
|
+
| `run.resources` data | Per-run data included in checkpoint recovery. Resume reopens handles; closures do not survive. |
|
|
9
11
|
| `run.artifacts` | Text files addressed by `{ path }` relative to this run's artifacts directory. Write content, pass the ref, read and validate at the consumer. |
|
|
10
12
|
| Outcome metadata | Caller-facing summary, artifact/log refs and small data, exposed by run inspection. |
|
|
11
13
|
| Workflow params | Explicit input to the current/next step, persisted for recovery. |
|
|
12
14
|
|
|
13
|
-
Workflow state is
|
|
15
|
+
Workflow state is a built-in [run resource](resources.md), available without an explicit `ensure` call. State declarations live in the manifest; declaring a field, including a schema default, does not set its value. The [agent example](../examples/agent-then-analysis/plugin.ts) saves the draft ref in run state and also passes it explicitly to analysis. The state value is a retained run record; the params are the consumer's input contract.
|
|
14
16
|
|
|
15
17
|
Artifact refs are paths, not content hashes, and a write to the same path replaces its content. State writes are serialized and atomic; an artifact write plus a state update is not a single transaction. An artifact read returns text, so a JSON consumer still needs parsing and schema validation.
|
|
16
18
|
|
package/assets/docs/projects.md
CHANGED
|
@@ -32,24 +32,15 @@ Reusable config files, conventionally `norn.json`, can declare `plugins`, `inclu
|
|
|
32
32
|
}
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
Plugin and include paths resolve relative to the file declaring them. `*` matches one directory segment. There is no automatic plugin tree scan, nor automatic inclusion of a sibling `norn.json`. Project config overrides included values; conflicting reusable values and duplicate plugin IDs are rejected. `version`
|
|
36
|
-
|
|
37
|
-
Projects using Seer can additionally declare writable project-relative roots:
|
|
38
|
-
|
|
39
|
-
```json
|
|
40
|
-
"seerMode": { "writableRoots": ["./workflow-sources"] }
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
This is a project-file field, not an OS sandbox. The current helper contract is in
|
|
44
|
-
[Seer exports](../packages/sdk/src/seer/index.ts) and [config resolution](../packages/sdk/src/seer/config.ts).
|
|
35
|
+
Plugin and include paths resolve relative to the file declaring them. `*` matches one directory segment. There is no automatic plugin tree scan, nor automatic inclusion of a sibling `norn.json`. Project config overrides included values; conflicting reusable values and duplicate plugin IDs are rejected. `version` belongs only in the project file.
|
|
45
36
|
|
|
46
37
|
## Import and reload
|
|
47
38
|
|
|
48
|
-
Each registered module default-exports `definePlugin(manifest, implementation)`. Norn loads TypeScript through jiti without a local build, supplying runtime imports for `@vimhead.dev/norn`, its `/files
|
|
39
|
+
Each registered module default-exports `definePlugin(manifest, implementation)`. Norn loads TypeScript through jiti without a local build, supplying runtime imports for `@vimhead.dev/norn`, its `/files` and `/schema` subpaths, `typebox`, `typebox/value`, `typebox/compile`, and `typebox/schema`. Other dependencies need normal package resolution from the plugin's location.
|
|
49
40
|
|
|
50
41
|
Runtime virtual imports do not configure TypeScript or an editor. A matching `@vimhead.dev/norn` installation provides the SDK types; the source checkout's examples are checked by its `tsconfig.json`. A successful runtime import alone is not a type check.
|
|
51
42
|
|
|
52
|
-
New CLI discovery/start/resume invocations load current source; an already executing workflow retains its loaded implementation. Module evaluation and implementation factories run even during discovery.
|
|
43
|
+
New CLI discovery/start/resume invocations load current source; an already executing workflow retains its loaded implementation. Module evaluation and implementation factories run even during discovery. [Persistence](persistence.md#choose-what-survives) defines the lifetime of factory context state.
|
|
53
44
|
|
|
54
45
|
| Decision | GOOD | BAD |
|
|
55
46
|
|---|---|---|
|
|
@@ -27,8 +27,9 @@ It is independent of the outer Pi harness's global configuration:
|
|
|
27
27
|
- `models.json` — custom endpoints, models, and authentication configuration
|
|
28
28
|
|
|
29
29
|
`NORN_AGENT_DIR` selects another directory. Set it consistently for both
|
|
30
|
-
`norn pi` and workflow execution. A programmatic runtime caller
|
|
31
|
-
|
|
30
|
+
`norn pi` and workflow execution. A programmatic runtime caller's explicit
|
|
31
|
+
`agentDir` takes precedence over `NORN_AGENT_DIR`; the setup command still needs
|
|
32
|
+
to target that same directory. Norn does not use an inherited
|
|
32
33
|
`PI_CODING_AGENT_DIR` to select its global configuration. Existing Pi packages and
|
|
33
34
|
credentials are not imported or linked: install providers and authenticate through
|
|
34
35
|
`norn pi`, even when Pi is the outer harness.
|
|
@@ -59,7 +60,7 @@ norn pi
|
|
|
59
60
|
|
|
60
61
|
This installs into Norn's global configuration. Select the new provider using
|
|
61
62
|
`/model`. Agent resource discovery and model precedence are covered in
|
|
62
|
-
[Norn agents](
|
|
63
|
+
[Norn agents](agents.md#prompts-tools-and-resource-loading).
|
|
63
64
|
Use `norn pi list` to inspect installations and `norn pi remove <source>` to remove
|
|
64
65
|
one. Upgrade bundled Pi by upgrading Norn, not by using Pi's self-update command.
|
|
65
66
|
`norn pi update --extensions` updates unpinned extension packages.
|
package/assets/docs/resources.md
CHANGED
|
@@ -2,18 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## Initialization is separate from attachment
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
`run.resources` provides shared resource handles, including [workflow state](persistence.md#choose-what-survives).
|
|
6
6
|
|
|
7
|
-
`run.resources.ensure(definition)` returns a shared handle
|
|
7
|
+
`run.resources.ensure(definition)` returns a shared handle for that resource across workflow contexts in the current executor. A definition contains `name`, `kind`, JSON `configuration`, and `initialize({directory, files, mode})`. Names are single alphanumeric/underscore/hyphen identifiers starting with an alphanumeric character. Identity/configuration conflicts fail; `configuration` owns format/version compatibility.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
`mode: "create"` also covers retry of an interrupted initialization; `mode: "open"` means an earlier initialization succeeded. Initializers own their data schema and must reject missing/incompatible data when reopening. Failed initialization remains visible and retryable, not a successful empty resource. Definitions have no filesystem effects until ensured.
|
|
10
10
|
|
|
11
11
|
| Decision | GOOD | BAD |
|
|
12
12
|
|---|---|---|
|
|
13
13
|
| IF implementing an initializer, THEN make create retries preserve existing data and open validate existing storage. ELSE do not register the definition. | Validate a file left by an interrupted create before reusing it. | Truncate the file each time `ensure` calls the initializer. |
|
|
14
14
|
| IF a resource format changes incompatibly, THEN change its declared configuration and provide an explicit migration. ELSE reopen the same format. | A mismatching `format` fails. | Reinterpret old bytes under an unchanged format declaration. |
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
[Persistence](persistence.md) covers resource lifetimes, storage locations, and recovery. Cross-run storage, queues, ledgers, and scheduler/agent activation are not supplied by this API.
|
|
17
17
|
|
|
18
18
|
## Explicit agent attachment
|
|
19
19
|
|
|
@@ -33,7 +33,7 @@ resourceAdapters: [StateAdapter({
|
|
|
33
33
|
|
|
34
34
|
List/get output is serialized JSON in bounded text pages. Requests specify UTF-16 `offset` and `limit` (1–10000); responses include `text`, `nextOffset` and a content `revision`. Pages are not a pinned snapshot. Set operations persist complete field values; get followed by set is not a transaction.
|
|
35
35
|
|
|
36
|
-
Custom adapters implement `NornAgentResourceAdapter`: a unique name and `bind({runId, label})` returning a `NornAgentResourceBinding` with tools and async `dispose()`. An adapter can expose one or several resource handles; initializing storage does not construct or attach tools. Session creation and one-shot prompting accept adapters through `resourceAdapters`, not through the resource manager or state handle directly.
|
|
36
|
+
Custom adapters implement `NornAgentResourceAdapter`: a unique name and `bind({runId, label})` returning a `NornAgentResourceBinding` with tools and async `dispose()`. An adapter can expose one or several resource handles; initializing storage does not construct or attach tools. Session creation and one-shot prompting accept adapters through `resourceAdapters`, not through the resource manager or state handle directly. Attached tools are activated with the normal response tool. Duplicate adapter names and collisions with built-ins, the response tool, other adapters or already-loaded extension tools fail. Successful bindings are cleaned up in reverse order on session disposal or later creation failure. An initializer/binder that throws before returning its handle owns cleanup of its partial allocations.
|
|
37
37
|
|
|
38
38
|
| Decision | GOOD | BAD |
|
|
39
39
|
|---|---|---|
|
|
@@ -47,9 +47,9 @@ The attachment never exposes internal scheduler/checkpoint control state. It is
|
|
|
47
47
|
|
|
48
48
|
`run.resources.files` supplies `readText`, `writeText`, and `withExclusiveLock(path, async lockedPath => ...)`. Standalone callers can construct `NornFileCoordinator({lockRoot, waitTimeoutMs})` from `@vimhead.dev/norn`. Coordinating callers must use the same lock namespace. Target parents must exist before a raw `withExclusiveLock` call; `writeText` creates them. Existing symbolic links resolve to their canonical target; dangling links fail.
|
|
49
49
|
|
|
50
|
-
The lock spans the complete callback, including read/validate/modify/persist.
|
|
50
|
+
The lock spans the complete callback, including read/validate/modify/persist. `writeText` replaces complete file contents atomically. A live owner is never expired by a TTL; confirmed dead local owners can be reclaimed. Invalid or foreign-host ownership fails closed, and contention has a bounded wait. PID reuse can delay reclamation rather than permit two owners. Locks support local filesystems on one host, not distributed storage.
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
Reads of live command-output logs may return partial streams. File locks do not make multi-file snapshots or external side effects transactional.
|
|
53
53
|
|
|
54
54
|
| Decision | GOOD | BAD |
|
|
55
55
|
|---|---|---|
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# TypeBox schemas
|
|
2
|
+
|
|
3
|
+
The [Norn SDK](../packages/sdk/src/api.ts) accepts native `typebox` 1.x schemas for workflow params, config, state and agent responses. This reference is checked against 1.3.33. [Project loading](projects.md#import-and-reload) describes runtime-provided imports and editor dependency resolution.
|
|
4
|
+
|
|
5
|
+
## TypeScript → TypeBox
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { Type, type Static } from "typebox";
|
|
9
|
+
|
|
10
|
+
const Person = Type.Object({ name: Type.String() });
|
|
11
|
+
type Person = Static<typeof Person>;
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Schemas are runtime values; `Static` derives the TypeScript type. In the table, `S`, `A` and `B` are schemas, with corresponding inferred types `T`, `TA` and `TB`.
|
|
15
|
+
|
|
16
|
+
| TypeScript type | TypeBox schema |
|
|
17
|
+
|---|---|
|
|
18
|
+
| `string` | `Type.String()` |
|
|
19
|
+
| `number` | `Type.Number()` |
|
|
20
|
+
| `boolean` | `Type.Boolean()` |
|
|
21
|
+
| `null` | `Type.Null()` |
|
|
22
|
+
| `undefined` | `Type.Undefined()` |
|
|
23
|
+
| `unknown` | `Type.Unknown()` |
|
|
24
|
+
| `any` | `Type.Any()` |
|
|
25
|
+
| `never` | `Type.Never()` |
|
|
26
|
+
| `"ready"` | `Type.Literal("ready")` |
|
|
27
|
+
| `string[]` | `Type.Array(Type.String())` |
|
|
28
|
+
| `[string, number]` | `Type.Tuple([Type.String(), Type.Number()])` |
|
|
29
|
+
| `{ name: string }` | `Type.Object({ name: Type.String() })` |
|
|
30
|
+
| `{ name?: string }` | `Type.Object({ name: Type.Optional(Type.String()) })` |
|
|
31
|
+
| `{ readonly name: string }` | `Type.Object({ name: Type.Readonly(Type.String()) })` |
|
|
32
|
+
| `string \| null` | `Type.Union([Type.String(), Type.Null()])` |
|
|
33
|
+
| `TA \| TB` | `Type.Union([A, B])` |
|
|
34
|
+
| `TA & TB` | `Type.Intersect([A, B])` |
|
|
35
|
+
| `Record<string, number>` | `Type.Record(Type.String(), Type.Number())` |
|
|
36
|
+
| `Partial<T>` | `Type.Partial(S)` |
|
|
37
|
+
| `Required<T>` | `Type.Required(S)` |
|
|
38
|
+
| `Pick<T, "name">` | `Type.Pick(S, ["name"])` |
|
|
39
|
+
| `Omit<T, "name">` | `Type.Omit(S, ["name"])` |
|
|
40
|
+
|
|
41
|
+
`Type.Integer()` also infers `number`; integrality is a runtime constraint. Optional properties may be absent; accepting `null` is a separate union. `Type.Readonly` affects static typing, not runtime freezing. `Type.Unknown` and `Type.Any` accept any value at runtime but infer different TypeScript types; neither guarantees JSON serializability. JavaScript-only values such as `undefined`, `bigint`, functions and symbols are not JSON data contracts.
|
|
42
|
+
|
|
43
|
+
## Constraints and validation
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { Type } from "typebox";
|
|
47
|
+
import { Value } from "typebox/value";
|
|
48
|
+
import { Compile } from "typebox/compile";
|
|
49
|
+
|
|
50
|
+
const Task = Type.Object({
|
|
51
|
+
title: Type.String({ minLength: 1 }),
|
|
52
|
+
attempts: Type.Integer({ minimum: 1, maximum: 10 }),
|
|
53
|
+
}, { additionalProperties: false });
|
|
54
|
+
|
|
55
|
+
const valid = Value.Check(Task, { title: "Build", attempts: 3 });
|
|
56
|
+
const invalid = Value.Check(Task, { title: "Build", attempts: "3" });
|
|
57
|
+
const issues = Value.Errors(Task, { title: "", attempts: 0 });
|
|
58
|
+
const validator = Compile(Task);
|
|
59
|
+
const compiledValid = validator.Check({ title: "Build", attempts: 3 });
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`valid` and `compiledValid` are `true`; `invalid` is `false`. `Value.Check` tests without repairing the input. `Value.Assert` throws on failure. `Value.Parse` returns validated input with the default `correctiveParse: false` setting; enabling that setting adds repair operations. `Value.Errors` returns issues with fields including `keyword`, `instancePath`, `schemaPath` and `message`. `Compile` creates a reusable validator.
|
|
63
|
+
|
|
64
|
+
Objects permit additional properties by default. `additionalProperties: false` rejects extras during validation; it does not strip them. A `default` annotation neither makes a property optional nor fills it during `Value.Check`. `Value.Default` is a separate operation; `Value.Convert` and `Value.Clean` likewise explicitly convert and clean values rather than merely validate them.
|
|
65
|
+
|
|
66
|
+
## Tagged unions and refinements
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
import { Type, type Static } from "typebox";
|
|
70
|
+
import { Value } from "typebox/value";
|
|
71
|
+
|
|
72
|
+
const Outcome = Type.Union([
|
|
73
|
+
Type.Object({ kind: Type.Literal("done"), summary: Type.String() }),
|
|
74
|
+
Type.Object({ kind: Type.Literal("retry"), reason: Type.String() }),
|
|
75
|
+
]);
|
|
76
|
+
type Outcome = Static<typeof Outcome>;
|
|
77
|
+
|
|
78
|
+
const UniqueNames = Type.Refine(
|
|
79
|
+
Type.Array(Type.String()),
|
|
80
|
+
names => new Set(names).size === names.length,
|
|
81
|
+
() => "Names must be unique",
|
|
82
|
+
);
|
|
83
|
+
const distinct = Value.Check(UniqueNames, ["build", "review"]);
|
|
84
|
+
const repeated = Value.Check(UniqueNames, ["build", "build"]);
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The literal `kind` supports TypeScript narrowing and distinguishes runtime branches. `Type.Refine` adds a predicate to an existing schema: `distinct` is `true`, `repeated` is `false`. The predicate executes in TypeBox; serializing the schema as JSON does not carry that function into another validator.
|
|
88
|
+
|
|
89
|
+
## Recursive data
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
import { Type, type Static } from "typebox";
|
|
93
|
+
import { Value } from "typebox/value";
|
|
94
|
+
|
|
95
|
+
const Tree = Type.Cyclic({
|
|
96
|
+
Node: Type.Object({
|
|
97
|
+
name: Type.String(),
|
|
98
|
+
children: Type.Array(Type.Ref("Node")),
|
|
99
|
+
}),
|
|
100
|
+
}, "Node");
|
|
101
|
+
type Tree = Static<typeof Tree>;
|
|
102
|
+
|
|
103
|
+
const tree: Tree = { name: "root", children: [{ name: "leaf", children: [] }] };
|
|
104
|
+
const validTree = Value.Check(Tree, tree);
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`Type.Cyclic` supplies definitions and selects the root; `Type.Ref` resolves a definition within that context. This describes recursive data shapes, not circular JavaScript object identities.
|
|
108
|
+
|
|
109
|
+
## Codecs and input/output types
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
import { Type, type StaticEncode, type StaticDecode } from "typebox";
|
|
113
|
+
import { Value } from "typebox/value";
|
|
114
|
+
|
|
115
|
+
const CountText = Type.Codec(Type.String({ pattern: "^[0-9]+$" }))
|
|
116
|
+
.Decode(text => Number(text))
|
|
117
|
+
.Encode(count => String(count));
|
|
118
|
+
|
|
119
|
+
type CountInput = StaticEncode<typeof CountText>;
|
|
120
|
+
type CountOutput = StaticDecode<typeof CountText>;
|
|
121
|
+
|
|
122
|
+
const count = Value.Decode(CountText, "42");
|
|
123
|
+
const text = Value.Encode(CountText, 42);
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
`CountInput` is `string`, `CountOutput` is `number`; the values are `42` and `"42"`. `Static` uses the encoded/input direction. `Type.Decode(schema, callback)` defines a decode-only codec instead of a bidirectional one.
|
|
127
|
+
|
|
128
|
+
`Value.Decode` is not a validation-only operation: in this version it clones, applies defaults, converts, cleans, validates, then runs decode callbacks. `Value.Check` and `Value.Assert` do not run codec callbacks. Callback behavior remains executable TypeBox code, not portable JSON Schema validation.
|
|
129
|
+
|
|
130
|
+
Further API reference: [TypeBox documentation](https://sinclairzx81.github.io/typebox/).
|
package/assets/docs/workflows.md
CHANGED
|
@@ -6,15 +6,16 @@ Start with the complete [minimal plugin](../examples/minimal-workflow/plugin.ts)
|
|
|
6
6
|
|
|
7
7
|
## Declaration and implementation
|
|
8
8
|
|
|
9
|
-
`definePluginManifest` qualifies workflow keys as `pluginId.workflowKey`, binds
|
|
9
|
+
`definePluginManifest` qualifies workflow keys as `pluginId.workflowKey`, binds TypeBox params, optional plugin config, and optional state declarations. `definePlugin` binds every declared key to an implementation. Entrypoints need nonempty caller-facing `instructions`; internal steps may omit them. `isEntrypoint` controls default catalogue visibility, not an authorization boundary: the CLI can start a known internal workflow ID directly.
|
|
10
10
|
|
|
11
|
-
`instructions` describe selection, inputs, effects, and outputs. They are neither a Norn agent system prompt nor a gate decision.
|
|
11
|
+
`instructions` describe selection, inputs, effects, and outputs. They are neither a Norn agent system prompt nor a gate decision. Declare params and config with [TypeBox schemas](schemas.md). Workflow inputs must be JSON data; `execute` receives the values after schema defaults and conversions. Public schemas must support `workflows inspect`.
|
|
12
12
|
|
|
13
13
|
The implementation's `execute(run, params, config)` returns one control result:
|
|
14
14
|
|
|
15
15
|
| Control | Meaning |
|
|
16
16
|
|---|---|
|
|
17
|
-
| `
|
|
17
|
+
| `target(params)` / `params.next(contribution)` | Select a known workflow or a caller-supplied next step. See [composition](composition.md). |
|
|
18
|
+
| `run.next(workflowId, params)` | Select a workflow by string ID; its input is checked at execution. |
|
|
18
19
|
| `run.complete(metadata)` | Complete the whole run, optionally exposing `summary`, `artifacts`, `logs`, and `data`. |
|
|
19
20
|
| `run.fail({ summary, ...metadata })` | Record failure with an actionable explanation and optional evidence. |
|
|
20
21
|
|
|
@@ -17,7 +17,7 @@ small application, not a prescribed development pipeline.
|
|
|
17
17
|
|
|
18
18
|
[Select the matching runtime](../../docs/cli.md#select-the-runtime), copy this
|
|
19
19
|
directory to a writable task directory, and `cd` into the copy. Configure
|
|
20
|
-
[Norn agent authentication and a default model](../../
|
|
20
|
+
[Norn agent authentication and a default model](../../docs/providers.md)
|
|
21
21
|
beforehand; this example makes live model calls and the detached executor cannot
|
|
22
22
|
prompt for login.
|
|
23
23
|
|