From 6f1db964022529f24b003b484058efe73430c78a Mon Sep 17 00:00:00 2001 From: Travis Nesland Date: Fri, 5 Jun 2026 11:23:48 -0400 Subject: [PATCH 1/2] fix(artifact): prefer Agentic hook env vars --- .agentic/hooks/artifact.written | 34 ++++++++++++++++--------------- src/cli/commands/artifact.test.ts | 8 ++++---- src/cli/commands/artifact.ts | 22 ++++++++++---------- 3 files changed, 33 insertions(+), 31 deletions(-) diff --git a/.agentic/hooks/artifact.written b/.agentic/hooks/artifact.written index 775ff21..809dd0d 100755 --- a/.agentic/hooks/artifact.written +++ b/.agentic/hooks/artifact.written @@ -1,27 +1,28 @@ #!/usr/bin/env bash # -# .spores/hooks/artifact.written +# .agentic/hooks/artifact.written # -# Fires after `spores artifact write ` or `spores artifact edit ` +# Fires after `agentic artifact write ` or `agentic artifact edit ` # has persisted a new version to disk. # Indexes the artifact reference into memory so it can be recalled by type, -# title, or version via `spores memory recall`. +# title, or version via `agentic memory recall`. # -# Contract — env vars set by spores: -# SPORES_EVENT = "artifact.written" | "artifact.edited" -# SPORES_BIN = path to the spores CLI entry (for callbacks) -# SPORES_ARTIFACT_ID = the artifact ULID -# SPORES_ARTIFACT_VERSION = the new version number (as a string) -# SPORES_ARTIFACT_MODE = write mode: "iterate" | "replace" +# Contract — env vars set by Agentic: +# AGENTIC_EVENT = "artifact.written" | "artifact.edited" +# AGENTIC_BIN = path to the Agentic CLI entry (for callbacks) +# AGENTIC_ARTIFACT_ID = the artifact ULID +# AGENTIC_ARTIFACT_VERSION = the new version number (as a string) +# AGENTIC_ARTIFACT_MODE = write mode: "iterate" | "replace" +# Legacy SPORES_* mirrors are also available during the compatibility window. # # Stdout from this script is appended to the command output. # Non-zero exit is surfaced as a warning — it does not fail the write. set -euo pipefail -ID="${SPORES_ARTIFACT_ID:-}" -VERSION="${SPORES_ARTIFACT_VERSION:-}" -MODE="${SPORES_ARTIFACT_MODE:-}" +ID="${AGENTIC_ARTIFACT_ID:-${SPORES_ARTIFACT_ID:-}}" +VERSION="${AGENTIC_ARTIFACT_VERSION:-${SPORES_ARTIFACT_VERSION:-}}" +MODE="${AGENTIC_ARTIFACT_MODE:-${SPORES_ARTIFACT_MODE:-}}" # Exit quietly if no artifact info to work with [[ -z "$ID" ]] && exit 0 @@ -29,11 +30,12 @@ MODE="${SPORES_ARTIFACT_MODE:-}" # Build memory content CONTENT="Artifact written: ${ID} v${VERSION} (mode: ${MODE})" -# SPORES_BIN may be a path to main.ts (dev) or the installed `spores` binary. -if [[ "$SPORES_BIN" == *.ts ]]; then - invoke() { bun "$SPORES_BIN" "$@"; } +# AGENTIC_BIN may be a path to main.ts (dev) or the installed `agentic` binary. +BIN="${AGENTIC_BIN:-${SPORES_BIN:-agentic}}" +if [[ "$BIN" == *.ts ]]; then + invoke() { bun "$BIN" "$@"; } else - invoke() { "$SPORES_BIN" "$@"; } + invoke() { "$BIN" "$@"; } fi # Store with a stable key so iterate writes update rather than duplicate diff --git a/src/cli/commands/artifact.test.ts b/src/cli/commands/artifact.test.ts index 8177dc9..02fabf5 100644 --- a/src/cli/commands/artifact.test.ts +++ b/src/cli/commands/artifact.test.ts @@ -129,7 +129,7 @@ describe("artifact CLI commands", () => { const hookScript = join(hooksDir, "artifact.created") await writeFile( hookScript, - `#!/usr/bin/env sh\necho "hook ran: $SPORES_ARTIFACT_ID"`, + `#!/usr/bin/env sh\necho "hook ran: $AGENTIC_ARTIFACT_ID / $SPORES_ARTIFACT_ID"`, ) await chmod(hookScript, 0o755) @@ -139,7 +139,7 @@ describe("artifact CLI commands", () => { const result = JSON.parse(out) expect(result.hook).toBeDefined() expect(result.hook.ran).toBe(true) - expect(result.hook.stdout).toContain("hook ran:") + expect(result.hook.stdout).toMatch(/hook ran: [0-9A-HJKMNP-TV-Z]{26} \/ [0-9A-HJKMNP-TV-Z]{26}/) }) }) @@ -361,7 +361,7 @@ describe("artifact CLI commands", () => { const hookScript = join(hooksDir, "artifact.finalized") await writeFile( hookScript, - `#!/usr/bin/env sh\necho "finalized: $SPORES_ARTIFACT_ID v$SPORES_ARTIFACT_FINAL_VERSION"`, + `#!/usr/bin/env sh\necho "finalized: $AGENTIC_ARTIFACT_ID v$AGENTIC_ARTIFACT_FINAL_VERSION / $SPORES_ARTIFACT_ID v$SPORES_ARTIFACT_FINAL_VERSION"`, ) await chmod(hookScript, 0o755) @@ -371,7 +371,7 @@ describe("artifact CLI commands", () => { const result = JSON.parse(out) expect(result.hook).toBeDefined() expect(result.hook.ran).toBe(true) - expect(result.hook.stdout).toContain("finalized:") + expect(result.hook.stdout).toMatch(/finalized: [0-9A-HJKMNP-TV-Z]{26} v1 \/ [0-9A-HJKMNP-TV-Z]{26} v1/) }) }) }) diff --git a/src/cli/commands/artifact.ts b/src/cli/commands/artifact.ts index 78efc62..e3fa043 100644 --- a/src/cli/commands/artifact.ts +++ b/src/cli/commands/artifact.ts @@ -88,10 +88,10 @@ export const artifactCreateCommand: Command = async (ctx, args, flags) => { const hook = await fireHook( "artifact.created", { - SPORES_ARTIFACT_ID: record.id, - SPORES_ARTIFACT_TYPE: record.type, - SPORES_ARTIFACT_TITLE: record.title, - SPORES_ARTIFACT_TAGS: record.tags.join(","), + AGENTIC_ARTIFACT_ID: record.id, + AGENTIC_ARTIFACT_TYPE: record.type, + AGENTIC_ARTIFACT_TITLE: record.title, + AGENTIC_ARTIFACT_TAGS: record.tags.join(","), }, ctx.baseDir, ) @@ -160,9 +160,9 @@ export const artifactWriteCommand: Command = async (ctx, args, flags) => { const hook = await fireHook( "artifact.written", { - SPORES_ARTIFACT_ID: record.id, - SPORES_ARTIFACT_VERSION: String(record.version), - SPORES_ARTIFACT_MODE: mode, + AGENTIC_ARTIFACT_ID: record.id, + AGENTIC_ARTIFACT_VERSION: String(record.version), + AGENTIC_ARTIFACT_MODE: mode, }, ctx.baseDir, ) @@ -196,8 +196,8 @@ export const artifactEditCommand: Command = async (ctx, args, flags) => { const hook = await fireHook( "artifact.edited", { - SPORES_ARTIFACT_ID: record.id, - SPORES_ARTIFACT_VERSION: String(record.version), + AGENTIC_ARTIFACT_ID: record.id, + AGENTIC_ARTIFACT_VERSION: String(record.version), }, ctx.baseDir, ) @@ -268,8 +268,8 @@ export const artifactFinalizeCommand: Command = async (ctx, args, _flags) => { const hook = await fireHook( "artifact.finalized", { - SPORES_ARTIFACT_ID: record.id, - SPORES_ARTIFACT_FINAL_VERSION: String(record.version), + AGENTIC_ARTIFACT_ID: record.id, + AGENTIC_ARTIFACT_FINAL_VERSION: String(record.version), }, ctx.baseDir, ) From a264272cf997058a0463bb56fb18b8a559e27a91 Mon Sep 17 00:00:00 2001 From: Travis Nesland Date: Fri, 5 Jun 2026 11:23:54 -0400 Subject: [PATCH 2/2] chore(dogfood): persist issue 16 design artifact --- .../01KTC4V2WZ5SKTS4H7E84RDD8H/meta.json | 17 ++++ .../01KTC4V2WZ5SKTS4H7E84RDD8H/v1.md | 90 ++++++++++++++++++ .../01KTC4V2WZ5SKTS4H7E84RDD8H/v2.md | 91 +++++++++++++++++++ .../tasks/01KNRZBRK1S3WAB2DTYG1TNTB5.json | 13 ++- 4 files changed, 207 insertions(+), 4 deletions(-) create mode 100644 .agentic/artifacts/01KTC4V2WZ5SKTS4H7E84RDD8H/meta.json create mode 100644 .agentic/artifacts/01KTC4V2WZ5SKTS4H7E84RDD8H/v1.md create mode 100644 .agentic/artifacts/01KTC4V2WZ5SKTS4H7E84RDD8H/v2.md diff --git a/.agentic/artifacts/01KTC4V2WZ5SKTS4H7E84RDD8H/meta.json b/.agentic/artifacts/01KTC4V2WZ5SKTS4H7E84RDD8H/meta.json new file mode 100644 index 0000000..bbbdbc0 --- /dev/null +++ b/.agentic/artifacts/01KTC4V2WZ5SKTS4H7E84RDD8H/meta.json @@ -0,0 +1,17 @@ +{ + "id": "01KTC4V2WZ5SKTS4H7E84RDD8H", + "type": "design", + "title": "Issue #16 Composition Object Design", + "body_ref": "01KTC4V2WZ5SKTS4H7E84RDD8H/v2.md", + "version": 2, + "finalized": true, + "tags": [ + "dogfood", + "design", + "scope", + "v0.2", + "issue-16" + ], + "created_at": "2026-06-05T15:01:19.391Z", + "updated_at": "2026-06-05T15:02:38.592Z" +} \ No newline at end of file diff --git a/.agentic/artifacts/01KTC4V2WZ5SKTS4H7E84RDD8H/v1.md b/.agentic/artifacts/01KTC4V2WZ5SKTS4H7E84RDD8H/v1.md new file mode 100644 index 0000000..be848f3 --- /dev/null +++ b/.agentic/artifacts/01KTC4V2WZ5SKTS4H7E84RDD8H/v1.md @@ -0,0 +1,90 @@ +# Issue #16 Composition Object Design + +## Dogfood Trigger + +The v0.5.0 dogfood run started exactly where the current workspace tells an agent to start: + +1. Activate `spores-maintainer`. +2. Check GitHub ready issues. +3. Run `task next`. + +That exposed two useful facts: + +- GitHub currently has no `ready` issues; the local task mirror still returns #16 as `ready`, so the mirror can drift. +- The persona's `task_filter` is visible only as metadata. The CLI does not apply it to `task next`, memory recall, skill foregrounding, or workflow defaults. + +This is the concrete pressure behind #16. + +## Converged Design + +### 1. What is the thing called? + +Use `Scope`. + +Do not grow workflow `Runtime` into a multi-primitive object. `Runtime` already means graph state machine. Do not use `Agentic` as a god-object. `Scope` is the bounded working context created by binding adapters plus an optional activated persona. + +### 2. What does Scope own? + +`Scope` owns defaults, not identity and not execution. + +It composes: + +- memory adapter plus optional default memory tag filter +- task adapter plus optional default task filter +- skill source plus optional foregrounded skill list +- workflow adapter/source plus optional default graph +- active persona metadata and rendered body + +It does not own model calls, transport, scheduling, approvals, auth, sessions, or `observed_by`. + +### 3. How do explicit arguments interact with persona defaults? + +Explicit call arguments win. + +Persona metadata supplies defaults when the caller omits a filter or graph. It is never a hard restriction. Example: a persona can default task queries to `{ status: "ready", tags: ["spores"] }`, but `scope.tasks.next({ tags: ["release"] })` should use the explicit release filter. + +### 4. How does hat switching work? + +One active persona at a time. + +`scope.activatePersona(name)` returns a new scope value or mutates a single active binding, but there is no stack and no merge semantics. Switching hats replaces the active binding. Deactivation returns to adapter-only defaults. + +### 5. What should the first API surface be? + +Keep it small and high-signal: + +- `Scope.fromConfig(baseDir)` +- `scope.activatePersona(name)` +- `scope.memory.recall(query, options?)` +- `scope.tasks.next(options?)` +- `scope.skills.list(options?)` +- `scope.workflows.defaultGraph()` or `scope.workflow.defaultGraph()` + +The first implementation should prove default injection across primitives, not introduce a session layer. + +### 6. What ordering problem did dogfood reveal? + +Filters are not enough. + +The original dogfood pain was not just "apply persona task_filter". Inside matching tasks, `task next` uses highest ULID/newest-first behavior. For real queues, callers need an ordering hint such as `created_at asc`, `updated_at desc`, or explicit priority. This should be designed separately from persona binding so `Scope` does not become a hidden scheduler. + +### 7. What should happen next? + +File implementation issues only after the design is accepted: + +1. Add task query ordering options to the task adapter/CLI. +2. Add `Scope` as a composition layer over existing adapters and sources. +3. Teach persona activation to produce a binding usable by `Scope`. +4. Update the dogfood on-ramp to use scoped calls once available. + +## Non-Goals + +- No session primitive. +- No identity field. +- No runtime hosting. +- No automatic GitHub task adapter in this issue. +- No persona stacking or inheritance. + +## Dogfood Notes + +The run also found a rename-era hook bug: artifact command hooks still populated only `SPORES_ARTIFACT_*` variables even though docs promised `AGENTIC_ARTIFACT_*` plus legacy mirrors. That fix belongs in v0.5.x because it affects the preferred Agentic hook contract directly. diff --git a/.agentic/artifacts/01KTC4V2WZ5SKTS4H7E84RDD8H/v2.md b/.agentic/artifacts/01KTC4V2WZ5SKTS4H7E84RDD8H/v2.md new file mode 100644 index 0000000..40fcc6a --- /dev/null +++ b/.agentic/artifacts/01KTC4V2WZ5SKTS4H7E84RDD8H/v2.md @@ -0,0 +1,91 @@ +# Issue #16 Composition Object Design + +## Dogfood Trigger + +The v0.5.0 dogfood run started exactly where the current workspace tells an agent to start: + +1. Activate `spores-maintainer`. +2. Check GitHub ready issues. +3. Run `task next`. + +That exposed three useful facts: + +- GitHub currently has no `ready` issues; the local task mirror still returns #16 as `ready`, so the mirror can drift. +- The persona's `task_filter` is visible only as metadata. The CLI does not apply it to `task next`, memory recall, skill foregrounding, or workflow defaults. +- Artifact hook docs promised preferred `AGENTIC_ARTIFACT_*` variables, but the artifact command path was still passing only legacy names before this run. + +This is the concrete pressure behind #16. + +## Converged Design + +### 1. What is the thing called? + +Use `Scope`. + +Do not grow workflow `Runtime` into a multi-primitive object. `Runtime` already means graph state machine. Do not use `Agentic` as a god-object. `Scope` is the bounded working context created by binding adapters plus an optional activated persona. + +### 2. What does Scope own? + +`Scope` owns defaults, not identity and not execution. + +It composes: + +- memory adapter plus optional default memory tag filter +- task adapter plus optional default task filter +- skill source plus optional foregrounded skill list +- workflow adapter/source plus optional default graph +- active persona metadata and rendered body + +It does not own model calls, transport, scheduling, approvals, auth, sessions, or `observed_by`. + +### 3. How do explicit arguments interact with persona defaults? + +Explicit call arguments win. + +Persona metadata supplies defaults when the caller omits a filter or graph. It is never a hard restriction. Example: a persona can default task queries to `{ status: "ready", tags: ["spores"] }`, but `scope.tasks.next({ tags: ["release"] })` should use the explicit release filter. + +### 4. How does hat switching work? + +One active persona at a time. + +`scope.activatePersona(name)` returns a new scope value or mutates a single active binding, but there is no stack and no merge semantics. Switching hats replaces the active binding. Deactivation returns to adapter-only defaults. + +### 5. What should the first API surface be? + +Keep it small and high-signal: + +- `Scope.fromConfig(baseDir)` +- `scope.activatePersona(name)` +- `scope.memory.recall(query, options?)` +- `scope.tasks.next(options?)` +- `scope.skills.list(options?)` +- `scope.workflows.defaultGraph()` or `scope.workflow.defaultGraph()` + +The first implementation should prove default injection across primitives, not introduce a session layer. + +### 6. What ordering problem did dogfood reveal? + +Filters are not enough. + +The original dogfood pain was not just "apply persona task_filter". Inside matching tasks, `task next` uses highest ULID/newest-first behavior. For real queues, callers need an ordering hint such as `created_at asc`, `updated_at desc`, or explicit priority. This should be designed separately from persona binding so `Scope` does not become a hidden scheduler. + +### 7. What should happen next? + +File implementation issues only after the design is accepted: + +1. Add task query ordering options to the task adapter/CLI. +2. Add `Scope` as a composition layer over existing adapters and sources. +3. Teach persona activation to produce a binding usable by `Scope`. +4. Update the dogfood on-ramp to use scoped calls once available. + +## Non-Goals + +- No session primitive. +- No identity field. +- No runtime hosting. +- No automatic GitHub task adapter in this issue. +- No persona stacking or inheritance. + +## Dogfood Notes + +The run found and fixed a rename-era hook contract bug: artifact command hooks now pass preferred `AGENTIC_ARTIFACT_*` variables and rely on the central hook bridge to provide legacy `SPORES_ARTIFACT_*` mirrors. The project `artifact.written` dogfood hook was updated to use `AGENTIC_*` first with legacy fallback. diff --git a/.agentic/tasks/01KNRZBRK1S3WAB2DTYG1TNTB5.json b/.agentic/tasks/01KNRZBRK1S3WAB2DTYG1TNTB5.json index e939d63..4c93d62 100644 --- a/.agentic/tasks/01KNRZBRK1S3WAB2DTYG1TNTB5.json +++ b/.agentic/tasks/01KNRZBRK1S3WAB2DTYG1TNTB5.json @@ -1,13 +1,18 @@ { "description": "Investigate v0.2 composition object: Runtime vs Scope vs Spores top-level", - "status": "ready", + "status": "blocked", "tags": [ "spores", "v0.2", "design" ], "id": "01KNRZBRK1S3WAB2DTYG1TNTB5", - "annotations": [], + "annotations": [ + { + "text": "2026-06-05 dogfood run posted the design artifact to GitHub issue #16; waiting on review/acceptance before implementation issues are filed.", + "timestamp": "2026-06-05T15:03:10.000Z" + } + ], "created_at": "2026-04-09T11:17:12.929Z", - "updated_at": "2026-04-09T11:17:12.929Z" -} \ No newline at end of file + "updated_at": "2026-06-05T15:03:10.000Z" +}