@kontourai/flow-agents 3.0.0 → 3.2.0
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/.github/workflows/ci.yml +12 -0
- package/CHANGELOG.md +32 -0
- package/CONTEXT.md +67 -1
- package/README.md +4 -0
- package/build/src/cli/assignment-provider.d.ts +188 -0
- package/build/src/cli/assignment-provider.js +193 -51
- package/build/src/cli/workflow-sidecar.d.ts +16 -6
- package/build/src/cli/workflow-sidecar.js +492 -45
- package/build/src/lib/flow-resolver.d.ts +12 -6
- package/build/src/lib/flow-resolver.js +30 -14
- package/build/src/tools/validate-source-tree.js +2 -1
- package/context/contracts/assignment-provider-contract.md +10 -1
- package/context/contracts/execution-contract.md +78 -0
- package/context/contracts/probe-docs-write-contract.md +187 -0
- package/context/scripts/hooks/config-protection.js +24 -4
- package/context/scripts/hooks/lib/config-protection-remedies.js +9 -0
- package/context/scripts/hooks/stop-goal-fit.js +262 -5
- package/context/scripts/hooks/workflow-steering.js +42 -0
- package/docs/adr/0001-flow-agents-consumes-flow.md +2 -0
- package/docs/adr/0002-flow-kits-as-extension-unit.md +2 -0
- package/docs/adr/0003-flow-agents-coordinates-kits-and-adapters.md +2 -0
- package/docs/adr/0004-gates-expect-surface-claims.md +2 -0
- package/docs/adr/0005-kubernetes-inspired-resource-contracts.md +2 -0
- package/docs/adr/0006-typescript-first-source-policy.md +2 -0
- package/docs/adr/0007-flow-skill-kit-tool-boundary.md +2 -0
- package/docs/adr/0007-skill-audit.md +2 -0
- package/docs/adr/0008-kit-operation-boundary.md +2 -0
- package/docs/adr/0009-canonical-hook-core-kit-boundary.md +2 -0
- package/docs/adr/0010-workflow-trust-state-as-hachure-bundle.md +2 -0
- package/docs/adr/0011-mcp-posture.md +2 -0
- package/docs/adr/0012-agent-coordination-as-liveness-claims.md +2 -0
- package/docs/adr/0013-context-lifecycle.md +2 -0
- package/docs/adr/0014-core-vs-domain-kit-boundary.md +2 -0
- package/docs/adr/0015-flow-flow-agents-boundary-reconciliation.md +2 -0
- package/docs/adr/0016-three-hard-boundary-model.md +2 -0
- package/docs/adr/0017-anti-gaming-trust-security-model.md +2 -0
- package/docs/adr/0018-freeze-local-shell-heuristics.md +2 -0
- package/docs/adr/0019-kit-dependency-ownership.md +2 -0
- package/docs/adr/0020-trust-reconcile-manifest-and-claim-classification.md +2 -0
- package/docs/adr/0021-assignment-leases-and-stale-claim-takeover.md +3 -1
- package/docs/adr/0022-fail-closed-delivery-reconciliation-with-governed-exemptions.md +113 -0
- package/docs/adr/README.md +49 -0
- package/docs/adr/index.md +34 -0
- package/docs/decisions/agent-coordination.md +20 -0
- package/docs/decisions/anti-gaming-trust-security.md +20 -0
- package/docs/decisions/context-lifecycle.md +18 -0
- package/docs/decisions/core-domain-kit-boundary.md +18 -0
- package/docs/decisions/flow-flow-agents-boundary.md +18 -0
- package/docs/decisions/flow-kit.md +20 -0
- package/docs/decisions/flow-skill-kit-tool-boundary.md +20 -0
- package/docs/decisions/graph-knowledge-provider.md +63 -0
- package/docs/decisions/hook-core-kit-boundary.md +18 -0
- package/docs/decisions/index.md +19 -0
- package/docs/decisions/kit-dependency-ownership.md +18 -0
- package/docs/decisions/kit-operation-boundary.md +18 -0
- package/docs/decisions/knowledge-promote-sub-flow.md +59 -0
- package/docs/decisions/kontour-resource-contract.md +18 -0
- package/docs/decisions/mcp-posture.md +18 -0
- package/docs/decisions/three-hard-boundary-model.md +18 -0
- package/docs/decisions/trust-reconcile.md +20 -0
- package/docs/decisions/typescript-source-policy.md +48 -0
- package/docs/decisions/workflow-enforcement.md +18 -0
- package/docs/decisions/workflow-trust-state.md +20 -0
- package/docs/fixture-ownership.md +1 -1
- package/docs/workflow-usage-guide.md +1 -1
- package/evals/ci/run-baseline.sh +6 -0
- package/evals/fixtures/assignment-provider/hostile-effective-state.json +11 -0
- package/evals/integration/test_checkpoint_signing.sh +4 -3
- package/evals/integration/test_current_json_per_actor.sh +516 -0
- package/evals/integration/test_ensure_session_ownership_guard.sh +435 -0
- package/evals/integration/test_gate_lockdown.sh +46 -0
- package/evals/integration/test_model_routing_escalation.sh +145 -0
- package/evals/integration/test_publish_delivery.sh +14 -6
- package/evals/integration/test_pull_work_liveness_preflight.sh +6 -1
- package/evals/integration/test_stop_hook_release.sh +552 -0
- package/evals/integration/test_trust_reconcile_negatives.sh +170 -0
- package/evals/integration/test_workflow_sidecar_writer.sh +12 -3
- package/evals/run.sh +10 -0
- package/evals/static/test_knowledge_providers.sh +13 -4
- package/evals/static/test_model_routing_hints.sh +107 -0
- package/evals/static/test_workflow_skills.sh +15 -2
- package/kits/builder/skills/builder-shape/SKILL.md +10 -0
- package/kits/builder/skills/deliver/SKILL.md +69 -11
- package/kits/builder/skills/design-probe/SKILL.md +47 -0
- package/kits/builder/skills/execute-plan/SKILL.md +13 -0
- package/kits/builder/skills/fix-bug/SKILL.md +17 -0
- package/kits/builder/skills/idea-to-backlog/SKILL.md +10 -0
- package/kits/builder/skills/pickup-probe/SKILL.md +4 -4
- package/kits/builder/skills/plan-work/SKILL.md +9 -0
- package/kits/builder/skills/pull-work/SKILL.md +10 -0
- package/kits/builder/skills/review-work/SKILL.md +11 -0
- package/kits/builder/skills/tdd-workflow/SKILL.md +17 -0
- package/kits/builder/skills/verify-work/SKILL.md +11 -0
- package/kits/knowledge/adapters/default-store/index.js +147 -18
- package/kits/knowledge/adapters/flow-runner/index.js +912 -16
- package/kits/knowledge/adapters/obsidian-store/index.js +97 -14
- package/kits/knowledge/adapters/shared/codec.js +265 -0
- package/kits/knowledge/docs/README.md +121 -2
- package/kits/knowledge/docs/store-contract.md +517 -7
- package/kits/knowledge/evals/audit-freshness/suite.test.js +92 -1
- package/kits/knowledge/evals/consolidate-incremental/suite.test.js +494 -0
- package/kits/knowledge/evals/consolidation/suite.test.js +1 -1
- package/kits/knowledge/evals/contract-suite/suite.test.js +212 -0
- package/kits/knowledge/evals/freshness/suite.test.js +339 -0
- package/kits/knowledge/evals/inbound-references/suite.test.js +351 -0
- package/kits/knowledge/evals/retirement/suite.test.js +1 -1
- package/kits/knowledge/evals/supersede-propagation/suite.test.js +384 -0
- package/kits/knowledge/flows/promote.flow.json +84 -0
- package/kits/knowledge/kit.json +15 -0
- package/kits/knowledge/promote/distill.js +96 -0
- package/kits/knowledge/promote/fixtures/contradiction-registry/CONTEXT.md +7 -0
- package/kits/knowledge/promote/fixtures/contradiction-registry/docs/decisions/cache-eviction-policy.md +12 -0
- package/kits/knowledge/promote/fixtures/contradiction-registry/docs/decisions/cache-eviction.md +12 -0
- package/kits/knowledge/promote/fixtures/repo/CONTEXT.md +11 -0
- package/kits/knowledge/promote/fixtures/repo/docs/decisions/index.md +9 -0
- package/kits/knowledge/promote/fixtures/repo/docs/decisions/liveness-claim.md +12 -0
- package/kits/knowledge/promote/fixtures/session/acceptance.json +9 -0
- package/kits/knowledge/promote/fixtures/session/agents/tool-planner/events.jsonl +1 -0
- package/kits/knowledge/promote/fixtures/session/agents/tool-verifier/events.jsonl +1 -0
- package/kits/knowledge/promote/fixtures/session/agents/tool-worker-1-1/events.jsonl +1 -0
- package/kits/knowledge/promote/fixtures/session/kontourai-flow-agents-287--deliver.md +47 -0
- package/kits/knowledge/promote/fixtures/session/learning.json +35 -0
- package/kits/knowledge/promote/fixtures/session/state.json +19 -0
- package/kits/knowledge/promote/health.js +137 -0
- package/kits/knowledge/promote/index.js +176 -0
- package/kits/knowledge/promote/ingest.js +103 -0
- package/kits/knowledge/promote/lib.js +132 -0
- package/kits/knowledge/promote/link.js +84 -0
- package/kits/knowledge/promote/promote.test.js +174 -0
- package/kits/knowledge/providers/conformance/suite.test.js +18 -0
- package/kits/knowledge/providers/index.js +1 -0
- package/kits/knowledge/providers/neo4j/connection.js +121 -0
- package/kits/knowledge/providers/neo4j/cypher.js +190 -0
- package/kits/knowledge/providers/neo4j/fake-driver.js +123 -0
- package/kits/knowledge/providers/neo4j/fixtures/spike-ground-truth.json +29 -0
- package/kits/knowledge/providers/neo4j/graph-queries.js +210 -0
- package/kits/knowledge/providers/neo4j/index.js +280 -0
- package/kits/knowledge/providers/neo4j/integration.test.js +122 -0
- package/kits/knowledge/providers/neo4j/neo4j.test.js +219 -0
- package/kits/knowledge/providers/neo4j/sync.js +235 -0
- package/package.json +4 -2
- package/schemas/workflow-handoff.schema.json +6 -0
- package/scripts/README.md +1 -0
- package/scripts/ci/mint-attestation.js +33 -6
- package/scripts/ci/trust-reconcile.js +144 -26
- package/scripts/freeze-adrs.mjs +364 -0
- package/scripts/hooks/config-protection.js +24 -4
- package/scripts/hooks/evidence-capture.js +4 -1
- package/scripts/hooks/lib/config-protection-remedies.js +9 -0
- package/scripts/hooks/lib/current-pointer.js +123 -0
- package/scripts/hooks/lib/liveness-heartbeat.js +75 -35
- package/scripts/hooks/stop-goal-fit.js +262 -5
- package/scripts/hooks/workflow-steering.js +42 -0
- package/scripts/statusline/flow-agents-statusline.js +3 -1
- package/src/cli/assignment-provider.ts +239 -59
- package/src/cli/workflow-sidecar.ts +529 -43
- package/src/lib/flow-resolver.ts +35 -14
- package/src/tools/validate-source-tree.ts +2 -1
|
@@ -13,6 +13,19 @@ Plan artifact in, implemented code out. Fans out to tool-worker subagents in par
|
|
|
13
13
|
|---|---|
|
|
14
14
|
| tool-worker | Implementation per task spec (up to 4 parallel) |
|
|
15
15
|
|
|
16
|
+
## Model Routing
|
|
17
|
+
|
|
18
|
+
Worker slices (`tool-worker`) route by task shape: `delegate-implementation` for
|
|
19
|
+
precisely-planned implementation (the default), `delegate-mechanical` for
|
|
20
|
+
fully-specified mechanical slices (issue sync, doc/scan bookkeeping),
|
|
21
|
+
`delegate-design` when a slice genuinely needs design latitude. Resolve the role
|
|
22
|
+
from `.datum/config.json` (`npx @kontourai/datum resolve <role> --json`) and pass
|
|
23
|
+
the model explicitly. On a review/verify gate failure of a slice, re-dispatch its
|
|
24
|
+
fix one tier higher and record the escalation. See
|
|
25
|
+
`context/contracts/execution-contract.md` § Delegation: Model Routing (and
|
|
26
|
+
§ Escalation on gate failure). Fallback: inherit the session model when
|
|
27
|
+
datum/config is absent, noted in the artifact.
|
|
28
|
+
|
|
16
29
|
## Orchestrator Rule
|
|
17
30
|
|
|
18
31
|
You do not write source files. You read the plan artifact, fan out tasks to tool-worker, and update the session file between waves.
|
|
@@ -20,6 +20,23 @@ Inherited from primitives + diagnosis:
|
|
|
20
20
|
| tool-verifier | verify-work |
|
|
21
21
|
| tool-playwright | diagnosis (reproduce) + verify-work |
|
|
22
22
|
|
|
23
|
+
## Model Routing
|
|
24
|
+
|
|
25
|
+
Delegates are spawned with an explicit model override resolved from
|
|
26
|
+
`.datum/config.json` via `npx @kontourai/datum resolve <role> --json` — see
|
|
27
|
+
`context/contracts/execution-contract.md` § Delegation: Model Routing:
|
|
28
|
+
|
|
29
|
+
| Delegate | Role |
|
|
30
|
+
|---|---|
|
|
31
|
+
| tool-worker | `delegate-mechanical` for fully-specified mechanical slices, `delegate-implementation` for precisely-planned implementation, `delegate-design` when a slice needs design latitude |
|
|
32
|
+
| tool-planner | `delegate-design` |
|
|
33
|
+
| tool-code-reviewer / tool-security-reviewer | `delegate-implementation` by default, raised to the worker's tier when higher — never below the tier of the checked work (Goodhart guard) |
|
|
34
|
+
| tool-verifier / tool-playwright | `delegate-implementation` by default, raised to the worker's tier when higher — never below the tier of the checked work (Goodhart guard) |
|
|
35
|
+
|
|
36
|
+
On a review/verify gate failure, re-dispatch the fix one tier higher and record
|
|
37
|
+
the escalation (contract § Escalation on gate failure). Fallback: inherit the
|
|
38
|
+
session model when datum/config is absent, noted in the artifact.
|
|
39
|
+
|
|
23
40
|
## Orchestrator Rule
|
|
24
41
|
|
|
25
42
|
You never use `read`, `glob`, `grep`, or `code` on source files. All codebase analysis goes through tool-planner. All review goes through review-work. All verification goes through tool-verifier or tool-playwright.
|
|
@@ -21,6 +21,16 @@ Convert raw ideas into shaped, prioritized, executable backlog without starting
|
|
|
21
21
|
- Keep separate ideas separate until a shared outcome, hard dependency, or sequencing reason justifies bundling them.
|
|
22
22
|
- Push back when the user blends unrelated ideas, and ask them to justify why the ideas belong together before shaping bundled work.
|
|
23
23
|
|
|
24
|
+
## Model Routing
|
|
25
|
+
|
|
26
|
+
When this skill delegates its shaping work (opportunity review, option
|
|
27
|
+
exploration, slicing), resolve the `delegate-design` role from
|
|
28
|
+
`.datum/config.json` (`npx @kontourai/datum resolve delegate-design --json`) and
|
|
29
|
+
pass the resolved model explicitly — shaping a raw idea into a backlog needs
|
|
30
|
+
design latitude, so it routes to the design tier. See
|
|
31
|
+
`context/contracts/execution-contract.md` § Delegation: Model Routing. Fallback:
|
|
32
|
+
inherit the session model when datum/config is absent, noted in the artifact.
|
|
33
|
+
|
|
24
34
|
## Artifact Contract
|
|
25
35
|
|
|
26
36
|
Create or update `.kontourai/flow-agents/<slug>/<slug>--idea-to-backlog.md` with:
|
|
@@ -115,15 +115,15 @@ When the gate fails, record `--status fail` with `--expectation pickup-probe-rea
|
|
|
115
115
|
|
|
116
116
|
|
|
117
117
|
|
|
118
|
-
## Docs
|
|
118
|
+
## Docs-Write Policy: Two-Delta Emission
|
|
119
119
|
|
|
120
|
-
`pickup-probe` may identify durable terminology and decision gaps, but it must keep documentation changes narrow:
|
|
120
|
+
`pickup-probe` may identify durable terminology and decision gaps, but it must keep documentation changes narrow. Per `context/contracts/probe-docs-write-contract.md`, a **decision subject** crystallizing during pickup probing (a durable, reusable answer to a named domain question — not an implementation detail or transient planning choice) gets both deltas in the same motion:
|
|
121
121
|
|
|
122
|
-
- Update `CONTEXT.md` inline only for glossary-style durable terminology decisions, using tight one- or two-sentence definitions and `_Avoid_` terms when useful. Keep implementation details out of `CONTEXT.md`.
|
|
122
|
+
- Update `CONTEXT.md` inline only for glossary-style durable terminology decisions, using tight one- or two-sentence definitions and `_Avoid_` terms when useful. Keep implementation details out of `CONTEXT.md`. Coin the subject term first if it is not already a glossary entry — vocabulary is the decision registry's topic namespace (`context/contracts/decision-registry-contract.md` § Slug rules) — or refine the existing entry when a decision changes what it says.
|
|
123
123
|
- If a `CONTEXT-MAP.md` exists, update the relevant context instead of assuming the root glossary owns the term.
|
|
124
124
|
- Create a lazy context file only when a resolved term or workflow concept has no existing home.
|
|
125
125
|
- Do not create context files for transient planning notes, open questions, or provider snapshots.
|
|
126
|
-
-
|
|
126
|
+
- Record a decision delta at `docs/decisions/<slug>.md`: consult `docs/decisions/index.md` first and propose revise-vs-create (an existing topic file, including a `needs-decision` stub, is revised in place; only an uncovered subject gets a new file), then run `npm run gen:decisions-index && npm run check:decisions`. Link this Probe's own session artifact as a `session-archive` evidence ref (transcript provenance). Never propose or create a numbered ADR — `docs/adr/` is frozen history; decisions go into the topic-keyed registry.
|
|
127
127
|
- Prefer workflow artifacts for pickup decisions, unresolved questions, accepted gaps, and route reasons until the work is accepted.
|
|
128
128
|
|
|
129
129
|
## Handoff To Plan Work
|
|
@@ -13,6 +13,15 @@ Goal + directory in, structured plan artifact out. Pure planning primitive.
|
|
|
13
13
|
|---|---|
|
|
14
14
|
| tool-planner | Codebase analysis, structured execution plan, writes plan artifact |
|
|
15
15
|
|
|
16
|
+
## Model Routing
|
|
17
|
+
|
|
18
|
+
Planning delegation (`tool-planner`) resolves the `delegate-design` role from
|
|
19
|
+
`.datum/config.json` (`npx @kontourai/datum resolve delegate-design --json`) and
|
|
20
|
+
passes the resolved model explicitly — turning a goal into a plan needs design
|
|
21
|
+
latitude, so it routes to the design tier. See
|
|
22
|
+
`context/contracts/execution-contract.md` § Delegation: Model Routing. Fallback:
|
|
23
|
+
inherit the session model when datum/config is absent, noted in the artifact.
|
|
24
|
+
|
|
16
25
|
## Orchestrator Rule
|
|
17
26
|
|
|
18
27
|
You do not read source files. You delegate to tool-planner and read the artifact it produces.
|
|
@@ -26,6 +26,16 @@ Select ready backlog work and prepare a bounded execution handoff without implem
|
|
|
26
26
|
- Every pull-work artifact must correlate to selected backlog refs, shepherding refs for active PRs/sidecars/issues being finished before new work, or `backlog_gap=true` with a route to `idea-to-backlog`; direct audits with no new selection must record `shepherding_item_ids` or `backlog_gap`, not free-floating implementation scope.
|
|
27
27
|
- A stale broad continuation instruction, such as "keep going", "pick up the next two", or "continue after merge", may allow queue inspection but must not bypass per-item pickup Probe evidence.
|
|
28
28
|
|
|
29
|
+
## Model Routing
|
|
30
|
+
|
|
31
|
+
Board selection, WIP/shepherding scans, dependency joins, liveness preflight, and
|
|
32
|
+
issue-sync-style bookkeeping are fully-specified mechanical work: when this skill
|
|
33
|
+
delegates them, resolve the `delegate-mechanical` role from `.datum/config.json`
|
|
34
|
+
(`npx @kontourai/datum resolve delegate-mechanical --json`) and pass the resolved
|
|
35
|
+
model explicitly. See `context/contracts/execution-contract.md` § Delegation:
|
|
36
|
+
Model Routing. Fallback: inherit the session model when datum/config is absent,
|
|
37
|
+
noted in the artifact.
|
|
38
|
+
|
|
29
39
|
## Inputs
|
|
30
40
|
|
|
31
41
|
- Repository or working directory.
|
|
@@ -29,6 +29,17 @@ Keeping them separate makes failures route cleanly:
|
|
|
29
29
|
| tool-dependencies-updater | Dependency review when package manifests, dependency manifests, package manager config, or lockfiles change |
|
|
30
30
|
| configured architecture/domain/IaC/policy reviewer | Optional reviewer when the project or user configures one |
|
|
31
31
|
|
|
32
|
+
## Model Routing
|
|
33
|
+
|
|
34
|
+
Review roles never resolve **below** the tier of the work they check (Goodhart
|
|
35
|
+
guard): default `delegate-implementation`, raised to match or exceed the tier
|
|
36
|
+
that produced the work under review — a reviewer of `delegate-design` work
|
|
37
|
+
resolves `delegate-design` or `orchestrator`, never a cheaper tier. Resolve the
|
|
38
|
+
role from `.datum/config.json` (`npx @kontourai/datum resolve <role> --json`) and
|
|
39
|
+
pass the model explicitly. See `context/contracts/execution-contract.md`
|
|
40
|
+
§ Delegation: Model Routing and § Goodhart guard. Fallback: inherit the session
|
|
41
|
+
model when datum/config is absent, noted in the artifact.
|
|
42
|
+
|
|
32
43
|
## Shared Contracts
|
|
33
44
|
|
|
34
45
|
Follow:
|
|
@@ -25,6 +25,23 @@ Same as deliver (inherited from primitives):
|
|
|
25
25
|
| tool-verifier | verify-work (with coverage check) |
|
|
26
26
|
| tool-playwright | verify-work (if UI) |
|
|
27
27
|
|
|
28
|
+
## Model Routing
|
|
29
|
+
|
|
30
|
+
Delegates are spawned with an explicit model override resolved from
|
|
31
|
+
`.datum/config.json` via `npx @kontourai/datum resolve <role> --json` — see
|
|
32
|
+
`context/contracts/execution-contract.md` § Delegation: Model Routing:
|
|
33
|
+
|
|
34
|
+
| Delegate | Role |
|
|
35
|
+
|---|---|
|
|
36
|
+
| tool-worker | `delegate-mechanical` for fully-specified mechanical slices, `delegate-implementation` for precisely-planned implementation (RED/GREEN/REFACTOR), `delegate-design` when a slice needs design latitude |
|
|
37
|
+
| tool-planner | `delegate-design` |
|
|
38
|
+
| tool-code-reviewer / tool-security-reviewer | `delegate-implementation` by default, raised to the worker's tier when higher — never below the tier of the checked work (Goodhart guard) |
|
|
39
|
+
| tool-verifier / tool-playwright | `delegate-implementation` by default, raised to the worker's tier when higher — never below the tier of the checked work (Goodhart guard) |
|
|
40
|
+
|
|
41
|
+
On a review/verify gate failure, re-dispatch the fix one tier higher and record
|
|
42
|
+
the escalation (contract § Escalation on gate failure). Fallback: inherit the
|
|
43
|
+
session model when datum/config is absent, noted in the artifact.
|
|
44
|
+
|
|
28
45
|
## Orchestrator Rule
|
|
29
46
|
|
|
30
47
|
Same as deliver: you never touch source files. You coordinate the primitives with TDD-specific context.
|
|
@@ -16,6 +16,17 @@ Verification is not critique. Run `review-work` first when the task needs mainta
|
|
|
16
16
|
| tool-verifier | Code verification, acceptance criteria checking, structured verdicts |
|
|
17
17
|
| tool-playwright | Visual verification, screenshots, accessibility checks |
|
|
18
18
|
|
|
19
|
+
## Model Routing
|
|
20
|
+
|
|
21
|
+
Verify roles never resolve **below** the tier of the work they check (Goodhart
|
|
22
|
+
guard): default `delegate-implementation`, raised to match or exceed the tier
|
|
23
|
+
that produced the work under verification — a verifier of `delegate-design` work
|
|
24
|
+
resolves `delegate-design` or `orchestrator`, never a cheaper tier. Resolve the
|
|
25
|
+
role from `.datum/config.json` (`npx @kontourai/datum resolve <role> --json`) and
|
|
26
|
+
pass the model explicitly. See `context/contracts/execution-contract.md`
|
|
27
|
+
§ Delegation: Model Routing and § Goodhart guard. Fallback: inherit the session
|
|
28
|
+
model when datum/config is absent, noted in the artifact.
|
|
29
|
+
|
|
19
30
|
## Orchestrator Rule
|
|
20
31
|
|
|
21
32
|
You do not review source files. You delegate to tool-verifier and tool-playwright, then read the verdict artifact.
|
|
@@ -16,6 +16,22 @@ import * as fs from "node:fs";
|
|
|
16
16
|
import * as path from "node:path";
|
|
17
17
|
import { randomUUID } from "node:crypto";
|
|
18
18
|
|
|
19
|
+
// Record-identity resolution layer (short-id prefix + slug aliases, issue #339).
|
|
20
|
+
// Single-sourced in the shared codec so both bundled adapters resolve
|
|
21
|
+
// identically; the rest of this adapter keeps its own zero-import helpers.
|
|
22
|
+
import {
|
|
23
|
+
resolveRecordId,
|
|
24
|
+
normalizeAliases,
|
|
25
|
+
emptyAliasIndex,
|
|
26
|
+
loadAliasIndex,
|
|
27
|
+
saveAliasIndex,
|
|
28
|
+
registerAliases,
|
|
29
|
+
// Record-carried freshness + derived staleness (issue #341, Addendum J).
|
|
30
|
+
freshnessPatch,
|
|
31
|
+
decodeFreshnessFields,
|
|
32
|
+
isRecordStale,
|
|
33
|
+
} from "../shared/codec.js";
|
|
34
|
+
|
|
19
35
|
// ---------------------------------------------------------------------------
|
|
20
36
|
// Error helpers
|
|
21
37
|
// ---------------------------------------------------------------------------
|
|
@@ -140,7 +156,17 @@ function parseYamlLines(lines, start, baseIndent) {
|
|
|
140
156
|
}
|
|
141
157
|
|
|
142
158
|
function unquote(s) {
|
|
143
|
-
if (
|
|
159
|
+
if (s.startsWith('"') && s.endsWith('"')) {
|
|
160
|
+
// Double-quoted scalars are escaped by yamlScalar; reverse those escapes
|
|
161
|
+
// (backslash, quote, newline, carriage-return, tab) left-to-right so a
|
|
162
|
+
// frontmatter value can carry newlines/quotes and round-trip intact. This
|
|
163
|
+
// keeps every scalar on ONE physical line, so an embedded "\n---\n" can
|
|
164
|
+
// never be mistaken for the frontmatter/body terminator (parseMarkdown).
|
|
165
|
+
return s.slice(1, -1).replace(/\\(["\\nrt])/g, (_, c) =>
|
|
166
|
+
c === "n" ? "\n" : c === "r" ? "\r" : c === "t" ? "\t" : c
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
if (s.startsWith("'") && s.endsWith("'")) {
|
|
144
170
|
return s.slice(1, -1);
|
|
145
171
|
}
|
|
146
172
|
return s;
|
|
@@ -198,9 +224,20 @@ function serializeYaml(obj, indent = 0) {
|
|
|
198
224
|
|
|
199
225
|
function yamlScalar(v) {
|
|
200
226
|
if (typeof v === "string") {
|
|
201
|
-
// Quote if it contains special chars
|
|
202
|
-
|
|
203
|
-
|
|
227
|
+
// Quote if it contains special chars (incl. any whitespace control char that
|
|
228
|
+
// would otherwise span lines — newline, carriage-return, tab).
|
|
229
|
+
if (/[:#\[\]{},&*?|<>=!%@`"'\n\r\t]/.test(v) || v.trim() !== v || v === "") {
|
|
230
|
+
// Escape backslash first, then quote and the control chars, so the value
|
|
231
|
+
// stays on ONE physical line. Reversed by unquote(). Without newline
|
|
232
|
+
// escaping a value like a multi-section proposal body would inject a raw
|
|
233
|
+
// "\n---\n" into the frontmatter and corrupt the record on read.
|
|
234
|
+
const escaped = v
|
|
235
|
+
.replace(/\\/g, "\\\\")
|
|
236
|
+
.replace(/"/g, '\\"')
|
|
237
|
+
.replace(/\n/g, "\\n")
|
|
238
|
+
.replace(/\r/g, "\\r")
|
|
239
|
+
.replace(/\t/g, "\\t");
|
|
240
|
+
return `"${escaped}"`;
|
|
204
241
|
}
|
|
205
242
|
return v;
|
|
206
243
|
}
|
|
@@ -340,6 +377,7 @@ export class DefaultKnowledgeStore {
|
|
|
340
377
|
this._root = path.resolve(storeRoot);
|
|
341
378
|
this._recordsDir = path.join(this._root, "records");
|
|
342
379
|
this._graphPath = path.join(this._root, "graph-index.json");
|
|
380
|
+
this._aliasPath = path.join(this._root, "alias-index.json");
|
|
343
381
|
fs.mkdirSync(this._recordsDir, { recursive: true });
|
|
344
382
|
}
|
|
345
383
|
|
|
@@ -351,12 +389,42 @@ export class DefaultKnowledgeStore {
|
|
|
351
389
|
return path.join(this._recordsDir, `${id}.md`);
|
|
352
390
|
}
|
|
353
391
|
|
|
392
|
+
// -- Record-identity resolution (short-id prefix + slug alias, issue #339) --
|
|
393
|
+
|
|
394
|
+
/** Does a record file exist for this exact full id? */
|
|
395
|
+
_idExists(id) {
|
|
396
|
+
return fs.existsSync(this._recordPath(id));
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/** All full record ids, derived cheaply from the records/ directory. */
|
|
400
|
+
_listIds() {
|
|
401
|
+
if (!fs.existsSync(this._recordsDir)) return [];
|
|
402
|
+
return fs.readdirSync(this._recordsDir)
|
|
403
|
+
.filter((f) => f.endsWith(".md"))
|
|
404
|
+
.map((f) => f.slice(0, -3));
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Resolve a query token (exact id, slug alias, or unambiguous id prefix) to a
|
|
409
|
+
* single full record id, or null when it resolves to nothing. Throws
|
|
410
|
+
* AMBIGUOUS_ID when a prefix matches more than one record.
|
|
411
|
+
*/
|
|
412
|
+
_resolveId(input) {
|
|
413
|
+
const aliasIndex = loadAliasIndex(this._aliasPath);
|
|
414
|
+
return resolveRecordId(input, {
|
|
415
|
+
idExists: (rid) => this._idExists(rid),
|
|
416
|
+
listIds: () => this._listIds(),
|
|
417
|
+
bySlug: aliasIndex.by_slug,
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
|
|
354
421
|
_readRecord(id) {
|
|
355
422
|
const p = this._recordPath(id);
|
|
356
423
|
if (!fs.existsSync(p)) return null;
|
|
357
424
|
const text = fs.readFileSync(p, "utf8");
|
|
358
425
|
const { meta, body } = parseMarkdown(text);
|
|
359
|
-
|
|
426
|
+
// Coerce record-carried freshness fields to canonical types (#341).
|
|
427
|
+
return decodeFreshnessFields({ ...meta, body });
|
|
360
428
|
}
|
|
361
429
|
|
|
362
430
|
_writeRecord(record) {
|
|
@@ -393,6 +461,19 @@ export class DefaultKnowledgeStore {
|
|
|
393
461
|
const id = input.id || randomUUID();
|
|
394
462
|
const now = this._now();
|
|
395
463
|
|
|
464
|
+
// Validate slug aliases (issue #339) and reserve them in the alias map
|
|
465
|
+
// BEFORE any write, so a SLUG_CONFLICT aborts create without a partial record.
|
|
466
|
+
const aliases = normalizeAliases(input.aliases);
|
|
467
|
+
let aliasIndex = null;
|
|
468
|
+
if (aliases.length) {
|
|
469
|
+
aliasIndex = loadAliasIndex(this._aliasPath);
|
|
470
|
+
registerAliases(aliasIndex, id, aliases);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Validate optional freshness fields (#341) before any write, so a malformed
|
|
474
|
+
// expires_at / ttl_seconds aborts create without a partial record.
|
|
475
|
+
const fresh = freshnessPatch(input);
|
|
476
|
+
|
|
396
477
|
// Merge explicit links + wikilinks from body
|
|
397
478
|
const explicitLinks = input.links || [];
|
|
398
479
|
const wikilinks = extractWikilinks(input.body || "");
|
|
@@ -404,9 +485,11 @@ export class DefaultKnowledgeStore {
|
|
|
404
485
|
title: input.title,
|
|
405
486
|
category: input.category,
|
|
406
487
|
tags: input.tags || [],
|
|
488
|
+
...(aliases.length ? { aliases } : {}),
|
|
407
489
|
status: "active",
|
|
408
490
|
created_at: now,
|
|
409
491
|
updated_at: now,
|
|
492
|
+
...fresh,
|
|
410
493
|
provenance: {
|
|
411
494
|
agent: input.provenance.agent,
|
|
412
495
|
...(input.provenance.session_id ? { session_id: input.provenance.session_id } : {}),
|
|
@@ -425,6 +508,9 @@ export class DefaultKnowledgeStore {
|
|
|
425
508
|
addLinksToGraph(graph, id, links);
|
|
426
509
|
saveGraph(this._graphPath, graph);
|
|
427
510
|
|
|
511
|
+
// Persist the alias map only after the record is on disk.
|
|
512
|
+
if (aliasIndex) saveAliasIndex(this._aliasPath, aliasIndex);
|
|
513
|
+
|
|
428
514
|
return id;
|
|
429
515
|
}
|
|
430
516
|
|
|
@@ -439,7 +525,7 @@ export class DefaultKnowledgeStore {
|
|
|
439
525
|
const record = this._readRecord(id);
|
|
440
526
|
if (!record) throw notFoundError(id);
|
|
441
527
|
|
|
442
|
-
const mutableKeys = ["title", "body", "category", "tags", "links"];
|
|
528
|
+
const mutableKeys = ["title", "body", "category", "tags", "links", "aliases", "expires_at", "ttl_seconds"];
|
|
443
529
|
const supplied = mutableKeys.filter((k) => fields[k] !== undefined);
|
|
444
530
|
if (supplied.length === 0)
|
|
445
531
|
throw missingEvidenceError("update: at least one mutable field must be supplied");
|
|
@@ -447,8 +533,24 @@ export class DefaultKnowledgeStore {
|
|
|
447
533
|
if (fields.category !== undefined && !validateCategory(fields.category))
|
|
448
534
|
throw missingEvidenceError(`update: invalid category: ${fields.category}`);
|
|
449
535
|
|
|
536
|
+
// Validate/normalize supplied freshness fields (#341). A field set to null/""
|
|
537
|
+
// clears it (patch value undefined → dropped by the serializer).
|
|
538
|
+
const fresh = freshnessPatch(fields);
|
|
539
|
+
|
|
450
540
|
const now = this._now();
|
|
451
541
|
|
|
542
|
+
// Slug aliases are append-only: supplied aliases are UNIONED with existing
|
|
543
|
+
// ones so a previously issued slug keeps resolving after a restructure (R3).
|
|
544
|
+
let mergedAliases = Array.isArray(record.aliases) ? record.aliases.slice() : [];
|
|
545
|
+
let aliasIndex = null;
|
|
546
|
+
if (fields.aliases !== undefined) {
|
|
547
|
+
const incoming = normalizeAliases(fields.aliases);
|
|
548
|
+
const seen = new Set(mergedAliases);
|
|
549
|
+
for (const s of incoming) if (!seen.has(s)) { seen.add(s); mergedAliases.push(s); }
|
|
550
|
+
aliasIndex = loadAliasIndex(this._aliasPath);
|
|
551
|
+
registerAliases(aliasIndex, id, mergedAliases);
|
|
552
|
+
}
|
|
553
|
+
|
|
452
554
|
// Merge links if updated
|
|
453
555
|
let newLinks = record.links || [];
|
|
454
556
|
if (fields.links !== undefined) {
|
|
@@ -465,6 +567,8 @@ export class DefaultKnowledgeStore {
|
|
|
465
567
|
...(fields.body !== undefined ? { body: fields.body } : {}),
|
|
466
568
|
...(fields.category !== undefined ? { category: fields.category } : {}),
|
|
467
569
|
...(fields.tags !== undefined ? { tags: fields.tags } : {}),
|
|
570
|
+
...(mergedAliases.length ? { aliases: mergedAliases } : {}),
|
|
571
|
+
...fresh,
|
|
468
572
|
links: newLinks,
|
|
469
573
|
updated_at: now,
|
|
470
574
|
mutation_log: [
|
|
@@ -486,6 +590,8 @@ export class DefaultKnowledgeStore {
|
|
|
486
590
|
saveGraph(this._graphPath, graph);
|
|
487
591
|
|
|
488
592
|
this._writeRecord(updated);
|
|
593
|
+
|
|
594
|
+
if (aliasIndex) saveAliasIndex(this._aliasPath, aliasIndex);
|
|
489
595
|
}
|
|
490
596
|
|
|
491
597
|
// -------------------------------------------------------------------------
|
|
@@ -843,7 +949,12 @@ export class DefaultKnowledgeStore {
|
|
|
843
949
|
// -------------------------------------------------------------------------
|
|
844
950
|
|
|
845
951
|
async get(id) {
|
|
846
|
-
|
|
952
|
+
// Accept an exact id, a slug alias, or an unambiguous id prefix (issue #339).
|
|
953
|
+
// Unresolved → null (unchanged missing-record semantics); ambiguous prefix
|
|
954
|
+
// → throws AMBIGUOUS_ID.
|
|
955
|
+
const resolvedId = this._resolveId(id);
|
|
956
|
+
if (!resolvedId) return null;
|
|
957
|
+
return this._readRecord(resolvedId);
|
|
847
958
|
}
|
|
848
959
|
|
|
849
960
|
// -------------------------------------------------------------------------
|
|
@@ -851,10 +962,13 @@ export class DefaultKnowledgeStore {
|
|
|
851
962
|
// -------------------------------------------------------------------------
|
|
852
963
|
|
|
853
964
|
async getLinks(id) {
|
|
965
|
+
// Resolve prefix/slug to a full id; fall back to the raw token when it
|
|
966
|
+
// resolves to nothing so unknown ids still return empty arrays (not throw).
|
|
967
|
+
const key = this._resolveId(id) || id;
|
|
854
968
|
const graph = loadGraph(this._graphPath);
|
|
855
969
|
return {
|
|
856
|
-
forward: (graph.forward[
|
|
857
|
-
reverse: (graph.reverse[
|
|
970
|
+
forward: (graph.forward[key] || []).map((l) => ({ ...l })),
|
|
971
|
+
reverse: (graph.reverse[key] || []).map((l) => ({ ...l })),
|
|
858
972
|
};
|
|
859
973
|
}
|
|
860
974
|
|
|
@@ -865,18 +979,19 @@ export class DefaultKnowledgeStore {
|
|
|
865
979
|
async listByCategory(category, options = {}) {
|
|
866
980
|
const records = this._allRecords();
|
|
867
981
|
const includeRetired = options.includeRetired === true;
|
|
982
|
+
// Derived staleness filter (#341): keep only records past their own effective
|
|
983
|
+
// expiry at `now` (injectable for tests). Absent → no staleness filtering.
|
|
984
|
+
const staleOnly = options.stale === true;
|
|
985
|
+
const nowMs = options.now !== undefined ? new Date(options.now).getTime() : Date.now();
|
|
986
|
+
const keep = (r) =>
|
|
987
|
+
(includeRetired || (r.status || "active") !== "retired") &&
|
|
988
|
+
(!staleOnly || isRecordStale(r, nowMs));
|
|
868
989
|
if (options.prefix) {
|
|
869
990
|
return records.filter(
|
|
870
|
-
(r) =>
|
|
871
|
-
(r.category === category || r.category.startsWith(`${category}.`)) &&
|
|
872
|
-
(includeRetired || (r.status || "active") !== "retired")
|
|
991
|
+
(r) => (r.category === category || r.category.startsWith(`${category}.`)) && keep(r)
|
|
873
992
|
);
|
|
874
993
|
}
|
|
875
|
-
return records.filter(
|
|
876
|
-
(r) =>
|
|
877
|
-
r.category === category &&
|
|
878
|
-
(includeRetired || (r.status || "active") !== "retired")
|
|
879
|
-
);
|
|
994
|
+
return records.filter((r) => r.category === category && keep(r));
|
|
880
995
|
}
|
|
881
996
|
|
|
882
997
|
// -------------------------------------------------------------------------
|
|
@@ -885,10 +1000,14 @@ export class DefaultKnowledgeStore {
|
|
|
885
1000
|
|
|
886
1001
|
async listByType(type, options = {}) {
|
|
887
1002
|
const includeRetired = options.includeRetired === true;
|
|
1003
|
+
// Derived staleness filter (#341) — see listByCategory.
|
|
1004
|
+
const staleOnly = options.stale === true;
|
|
1005
|
+
const nowMs = options.now !== undefined ? new Date(options.now).getTime() : Date.now();
|
|
888
1006
|
return this._allRecords().filter(
|
|
889
1007
|
(r) =>
|
|
890
1008
|
r.type === type &&
|
|
891
|
-
(includeRetired || (r.status || "active") !== "retired")
|
|
1009
|
+
(includeRetired || (r.status || "active") !== "retired") &&
|
|
1010
|
+
(!staleOnly || isRecordStale(r, nowMs))
|
|
892
1011
|
);
|
|
893
1012
|
}
|
|
894
1013
|
|
|
@@ -911,6 +1030,16 @@ export class DefaultKnowledgeStore {
|
|
|
911
1030
|
const links = Object.values(rebuilt.forward).reduce((n, arr) => n + arr.length, 0);
|
|
912
1031
|
const changed = canonicalGraph(loadGraph(this._graphPath)) !== canonicalGraph(rebuilt);
|
|
913
1032
|
saveGraph(this._graphPath, rebuilt);
|
|
1033
|
+
|
|
1034
|
+
// The alias map is likewise derived from records' `aliases` — rebuild it so
|
|
1035
|
+
// a lost/hand-edited alias-index.json recovers on the same recovery path.
|
|
1036
|
+
const rebuiltAliases = emptyAliasIndex();
|
|
1037
|
+
for (const record of records) {
|
|
1038
|
+
const slugs = normalizeAliases(record.aliases);
|
|
1039
|
+
if (slugs.length) registerAliases(rebuiltAliases, record.id, slugs);
|
|
1040
|
+
}
|
|
1041
|
+
saveAliasIndex(this._aliasPath, rebuiltAliases);
|
|
1042
|
+
|
|
914
1043
|
return {
|
|
915
1044
|
records: records.length,
|
|
916
1045
|
links,
|