@kontourai/flow-agents 2.4.0 → 3.0.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/CODEOWNERS +8 -0
- package/.github/workflows/ci.yml +12 -0
- package/.github/workflows/trust-reconcile.yml +62 -4
- package/CHANGELOG.md +23 -0
- package/CONTEXT.md +21 -0
- package/build/src/cli/assignment-provider.d.ts +1 -0
- package/build/src/cli/assignment-provider.js +748 -0
- package/build/src/cli/effective-assignment-provider-settings.d.ts +1 -0
- package/build/src/cli/effective-assignment-provider-settings.js +125 -0
- package/build/src/cli/validate-workflow-artifacts.js +5 -1
- package/build/src/cli/workflow-sidecar.js +157 -110
- package/build/src/cli.js +6 -0
- package/build/src/tools/validate-source-tree.js +1 -0
- package/context/contracts/artifact-contract.md +2 -0
- package/context/contracts/assignment-provider-contract.md +239 -0
- package/context/contracts/builder-kit-workflow-state-contract.md +2 -0
- package/context/contracts/decision-registry-contract.md +2 -0
- package/context/contracts/delivery-contract.md +2 -0
- package/context/contracts/execution-contract.md +25 -0
- package/context/contracts/governance-adapter-contract.md +2 -0
- package/context/contracts/knowledge-store-contract.md +197 -0
- package/context/contracts/planning-contract.md +2 -0
- package/context/contracts/review-contract.md +2 -0
- package/context/contracts/sandbox-policy.md +2 -0
- package/context/contracts/standing-directives.md +13 -0
- package/context/contracts/verification-contract.md +2 -0
- package/context/contracts/work-item-contract.md +2 -0
- package/context/settings/assignment-provider-settings.json +33 -0
- package/docs/adr/0022-fail-closed-delivery-reconciliation-with-governed-exemptions.md +180 -0
- package/docs/context-map.md +1 -0
- package/docs/decisions/index.md +3 -0
- package/docs/decisions/knowledge-store-provider.md +51 -0
- package/docs/decisions/model-routing.md +63 -0
- package/docs/decisions/standing-directives.md +66 -0
- package/docs/fixture-ownership.md +1 -0
- package/docs/workflow-shared-contracts.md +2 -1
- package/docs/workflow-usage-guide.md +6 -0
- package/evals/ci/run-baseline.sh +6 -0
- package/evals/fixtures/assignment-provider/actor-a.json +6 -0
- package/evals/fixtures/assignment-provider/actor-b.json +6 -0
- package/evals/fixtures/assignment-provider/github-issue-claimed.json +27 -0
- package/evals/fixtures/assignment-provider/github-issue-unassigned.json +7 -0
- package/evals/fixtures/assignment-provider/liveness-fresh.json +9 -0
- package/evals/fixtures/assignment-provider/liveness-stale.json +9 -0
- package/evals/integration/test_assignment_provider_github.sh +318 -0
- package/evals/integration/test_assignment_provider_local_file.sh +222 -0
- package/evals/integration/test_critique_supersession_roundtrip.sh +182 -0
- package/evals/integration/test_fixture_retirement_audit.sh +2 -2
- package/evals/integration/test_publish_delivery.sh +21 -4
- package/evals/integration/test_pull_work_assignment_join.sh +132 -0
- package/evals/integration/test_pull_work_liveness_preflight.sh +14 -6
- package/evals/integration/test_reconcile_soundness.sh +33 -9
- package/evals/integration/test_trust_reconcile.sh +9 -8
- package/evals/integration/test_trust_reconcile_negatives.sh +608 -0
- package/evals/integration/test_workflow_sidecar_writer.sh +79 -0
- package/evals/run.sh +10 -0
- package/evals/static/test_flowdef_codeowners_coverage.sh +6 -0
- package/evals/static/test_knowledge_providers.sh +23 -0
- package/kits/builder/skills/deliver/SKILL.md +19 -0
- package/kits/builder/skills/pull-work/SKILL.md +78 -10
- package/kits/knowledge/kit.json +35 -0
- package/kits/knowledge/providers/conformance/fixtures/git-repo/CONTEXT.md +12 -0
- package/kits/knowledge/providers/conformance/fixtures/git-repo/docs/decisions/old-sprocket-shape.md +13 -0
- package/kits/knowledge/providers/conformance/fixtures/git-repo/docs/decisions/sprocket-shape.md +14 -0
- package/kits/knowledge/providers/conformance/fixtures/git-repo/docs/decisions/widget-format.md +14 -0
- package/kits/knowledge/providers/conformance/fixtures/git-repo/docs/learnings/fixture-learning.md +7 -0
- package/kits/knowledge/providers/conformance/fixtures/work-item/issues.json +30 -0
- package/kits/knowledge/providers/conformance/suite.test.js +125 -0
- package/kits/knowledge/providers/git-repo/index.js +236 -0
- package/kits/knowledge/providers/health/health-pass.test.js +99 -0
- package/kits/knowledge/providers/health/index.js +153 -0
- package/kits/knowledge/providers/index.js +24 -0
- package/kits/knowledge/providers/lib/model.js +91 -0
- package/kits/knowledge/providers/lib/schema-validate.js +119 -0
- package/kits/knowledge/providers/markdown-vault/index.js +169 -0
- package/kits/knowledge/providers/work-item/index.js +204 -0
- package/package.json +4 -2
- package/schemas/assignment-provider-settings.schema.json +125 -0
- package/schemas/knowledge/edge.schema.json +54 -0
- package/schemas/knowledge/health-report.schema.json +45 -0
- package/schemas/knowledge/node.schema.json +49 -0
- package/schemas/knowledge/proposal.schema.json +53 -0
- package/scripts/ci/trust-reconcile.js +521 -24
- package/src/cli/assignment-provider.ts +845 -0
- package/src/cli/effective-assignment-provider-settings.ts +112 -0
- package/src/cli/validate-workflow-artifacts.ts +5 -1
- package/src/cli/workflow-sidecar.ts +147 -106
- package/src/cli.ts +6 -0
- package/src/tools/validate-source-tree.ts +1 -0
|
@@ -2791,6 +2791,85 @@ else
|
|
|
2791
2791
|
_fail "trust.bundle capture-authoritative setup failed: $(cat "$TMPDIR_EVAL/tb-capture-evidence.out" "$TMPDIR_EVAL/tb-capture-evidence.err")"
|
|
2792
2792
|
fi
|
|
2793
2793
|
|
|
2794
|
+
# ---- #347: hard cutover -- an unstamped (pre-#344) claim in a session trust bundle is a loud,
|
|
2795
|
+
# typed error; checksFromBundle/critiquesFromBundle carry NO claimType-derivation fallback (that
|
|
2796
|
+
# fallback WAS the #268 defect kept reachable). ----------------------------------------------
|
|
2797
|
+
TB_UNSTAMPED_DIR="$TMPDIR_EVAL/repo/.kontourai/flow-agents/trust-bundle-unstamped"
|
|
2798
|
+
mkdir -p "$TB_UNSTAMPED_DIR"
|
|
2799
|
+
cp "$ARTIFACT_DIR/auto-sidecars--deliver.md" "$TB_UNSTAMPED_DIR/trust-bundle-unstamped--deliver.md"
|
|
2800
|
+
flow_agents_node "$WRITER" init-plan "$TB_UNSTAMPED_DIR/trust-bundle-unstamped--deliver.md" \
|
|
2801
|
+
--source-request "Unstamped bundle regression fixture." \
|
|
2802
|
+
--summary "Unstamped bundle regression fixture." \
|
|
2803
|
+
--next-action "Seed a stamped check, then strip its origin stamp to simulate a pre-#344 bundle." \
|
|
2804
|
+
--timestamp "2026-05-09T00:00:00Z" >"$TMPDIR_EVAL/tb-unstamped-init.out" 2>"$TMPDIR_EVAL/tb-unstamped-init.err"
|
|
2805
|
+
flow_agents_node "$WRITER" record-evidence "$TB_UNSTAMPED_DIR" \
|
|
2806
|
+
--verdict pass \
|
|
2807
|
+
--check-json '{"id":"tb-unstamped-check","kind":"test","status":"pass","summary":"Stamped check, about to be corrupted."}' \
|
|
2808
|
+
--timestamp "2026-05-09T00:01:00Z" >"$TMPDIR_EVAL/tb-unstamped-evidence.out" 2>"$TMPDIR_EVAL/tb-unstamped-evidence.err"
|
|
2809
|
+
|
|
2810
|
+
# Simulate a pre-#344 bundle by stripping the origin/check_kind stamp from every claim -- never
|
|
2811
|
+
# done by hand-editing production sessions, only here to recreate the on-disk shape of a bundle
|
|
2812
|
+
# recorded before #344 shipped stamping.
|
|
2813
|
+
export UNSTAMPED_TARGET="$TB_UNSTAMPED_DIR/trust.bundle"
|
|
2814
|
+
node --input-type=module <<NODEOF
|
|
2815
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
2816
|
+
const target = process.env.UNSTAMPED_TARGET;
|
|
2817
|
+
const data = JSON.parse(readFileSync(target, 'utf8'));
|
|
2818
|
+
data.claims = (data.claims || []).map((c) => {
|
|
2819
|
+
if (c && c.metadata && typeof c.metadata === 'object') {
|
|
2820
|
+
const rest = { ...c.metadata };
|
|
2821
|
+
delete rest.origin;
|
|
2822
|
+
delete rest.check_kind;
|
|
2823
|
+
return { ...c, metadata: rest };
|
|
2824
|
+
}
|
|
2825
|
+
return c;
|
|
2826
|
+
});
|
|
2827
|
+
writeFileSync(target, JSON.stringify(data, null, 2) + '\n');
|
|
2828
|
+
NODEOF
|
|
2829
|
+
|
|
2830
|
+
if flow_agents_node "$WRITER" record-critique "$TB_UNSTAMPED_DIR" \
|
|
2831
|
+
--id tb-unstamped-review \
|
|
2832
|
+
--reviewer tool-code-reviewer \
|
|
2833
|
+
--verdict pass \
|
|
2834
|
+
--summary "Should never be recorded -- the bundle is unstamped." \
|
|
2835
|
+
--timestamp "2026-05-09T00:02:00Z" >"$TMPDIR_EVAL/tb-unstamped-critique.out" 2>"$TMPDIR_EVAL/tb-unstamped-critique.err"; then
|
|
2836
|
+
_fail "record-critique should refuse to read an unstamped (pre-#344) trust.bundle, not silently reclassify it by claimType"
|
|
2837
|
+
elif rg -q 'pre-supersession trust\.bundle' "$TMPDIR_EVAL/tb-unstamped-critique.out" "$TMPDIR_EVAL/tb-unstamped-critique.err" \
|
|
2838
|
+
&& rg -qF "$TB_UNSTAMPED_DIR" "$TMPDIR_EVAL/tb-unstamped-critique.out" "$TMPDIR_EVAL/tb-unstamped-critique.err" \
|
|
2839
|
+
&& rg -q 're-record evidence to regenerate' "$TMPDIR_EVAL/tb-unstamped-critique.out" "$TMPDIR_EVAL/tb-unstamped-critique.err" \
|
|
2840
|
+
&& rg -q 'record-evidence' "$TMPDIR_EVAL/tb-unstamped-critique.out" "$TMPDIR_EVAL/tb-unstamped-critique.err"; then
|
|
2841
|
+
_pass "record-critique refuses an unstamped (pre-#344) trust.bundle: typed error names the session dir and the record-evidence remedy (no claimType-derivation fallback, #347)"
|
|
2842
|
+
else
|
|
2843
|
+
_fail "record-critique on an unstamped bundle failed for the wrong reason: $(cat "$TMPDIR_EVAL/tb-unstamped-critique.out" "$TMPDIR_EVAL/tb-unstamped-critique.err")"
|
|
2844
|
+
fi
|
|
2845
|
+
|
|
2846
|
+
# The same unstamped bundle also refuses promote (checksFromBundle) -- not just
|
|
2847
|
+
# record-critique/critiquesFromBundle.
|
|
2848
|
+
if flow_agents_node "$WRITER" promote "$TB_UNSTAMPED_DIR" --none --reason "regression probe" \
|
|
2849
|
+
--timestamp "2026-05-09T00:03:00Z" >"$TMPDIR_EVAL/tb-unstamped-promote.out" 2>"$TMPDIR_EVAL/tb-unstamped-promote.err"; then
|
|
2850
|
+
_fail "promote should refuse to read an unstamped (pre-#344) trust.bundle via checksFromBundle"
|
|
2851
|
+
elif rg -q 'pre-supersession trust\.bundle' "$TMPDIR_EVAL/tb-unstamped-promote.out" "$TMPDIR_EVAL/tb-unstamped-promote.err"; then
|
|
2852
|
+
_pass "promote (checksFromBundle) also refuses an unstamped (pre-#344) trust.bundle with the same typed error"
|
|
2853
|
+
else
|
|
2854
|
+
_fail "promote on an unstamped bundle failed for the wrong reason: $(cat "$TMPDIR_EVAL/tb-unstamped-promote.out" "$TMPDIR_EVAL/tb-unstamped-promote.err")"
|
|
2855
|
+
fi
|
|
2856
|
+
|
|
2857
|
+
# (b) STAMPED bundles are unaffected: the earlier, still-stamped trust-bundle-critique fixture
|
|
2858
|
+
# carries the origin stamp on every claim -- the normal write path was never touched by this cutover.
|
|
2859
|
+
export STAMPED_TARGET="$TB_CRITIQUE_DIR/trust.bundle"
|
|
2860
|
+
if [[ -f "$STAMPED_TARGET" ]] && node --input-type=module <<NODEOF 2>"$TMPDIR_EVAL/tb-stamped-unaffected.err"
|
|
2861
|
+
import { readFileSync } from 'node:fs';
|
|
2862
|
+
const target = process.env.STAMPED_TARGET;
|
|
2863
|
+
const bundle = JSON.parse(readFileSync(target, 'utf8'));
|
|
2864
|
+
const unstamped = bundle.claims.filter((c) => !(c.metadata && typeof c.metadata === 'object' && typeof c.metadata.origin === 'string' && c.metadata.origin.length > 0));
|
|
2865
|
+
if (unstamped.length > 0) { process.stderr.write('found ' + unstamped.length + ' claim(s) with no origin stamp in a normally-written bundle\n'); process.exit(1); }
|
|
2866
|
+
NODEOF
|
|
2867
|
+
then
|
|
2868
|
+
_pass "stamped bundles are unaffected: every claim in a normally-written trust bundle carries the origin stamp (#347)"
|
|
2869
|
+
else
|
|
2870
|
+
_fail "stamped-bundle regression: $(cat "$TMPDIR_EVAL/tb-stamped-unaffected.err")"
|
|
2871
|
+
fi
|
|
2872
|
+
|
|
2794
2873
|
# ─── AC4: render-trust-panel projects the bundle to a standalone Surface Trust Panel (ADR 0010 Phase 3) ──
|
|
2795
2874
|
if [[ -f "$TB_CAPTURE_DIR/trust.bundle" ]] && flow_agents_node "$WRITER" render-trust-panel "$TB_CAPTURE_DIR" --out "$TB_CAPTURE_DIR/trust-panel.html" >"$TMPDIR_EVAL/tb-panel.out" 2>"$TMPDIR_EVAL/tb-panel.err"; then
|
|
2796
2875
|
PANEL="$TB_CAPTURE_DIR/trust-panel.html"
|
package/evals/run.sh
CHANGED
|
@@ -146,6 +146,8 @@ run_static() {
|
|
|
146
146
|
bash "$EVAL_DIR/static/test_flowdef_codeowners_coverage.sh" || result=1
|
|
147
147
|
echo ""
|
|
148
148
|
bash "$EVAL_DIR/static/test_unit_helpers.sh" || result=1
|
|
149
|
+
echo ""
|
|
150
|
+
bash "$EVAL_DIR/static/test_knowledge_providers.sh" || result=1
|
|
149
151
|
return $result
|
|
150
152
|
}
|
|
151
153
|
|
|
@@ -195,6 +197,12 @@ run_integration() {
|
|
|
195
197
|
echo ""
|
|
196
198
|
bash "$EVAL_DIR/integration/test_actor_identity.sh" || result=1
|
|
197
199
|
echo ""
|
|
200
|
+
bash "$EVAL_DIR/integration/test_assignment_provider_local_file.sh" || result=1
|
|
201
|
+
echo ""
|
|
202
|
+
bash "$EVAL_DIR/integration/test_assignment_provider_github.sh" || result=1
|
|
203
|
+
echo ""
|
|
204
|
+
bash "$EVAL_DIR/integration/test_pull_work_assignment_join.sh" || result=1
|
|
205
|
+
echo ""
|
|
198
206
|
bash "$EVAL_DIR/integration/test_liveness_heartbeat.sh" || result=1
|
|
199
207
|
echo ""
|
|
200
208
|
bash "$EVAL_DIR/integration/test_pull_work_liveness_preflight.sh" || result=1
|
|
@@ -241,6 +249,8 @@ run_integration() {
|
|
|
241
249
|
echo ""
|
|
242
250
|
bash "$EVAL_DIR/integration/test_flowdef_session_history_preservation.sh" || result=1
|
|
243
251
|
echo ""
|
|
252
|
+
bash "$EVAL_DIR/integration/test_critique_supersession_roundtrip.sh" || result=1
|
|
253
|
+
echo ""
|
|
244
254
|
bash "$EVAL_DIR/integration/test_flowdef_session_activation.sh" || result=1
|
|
245
255
|
echo ""
|
|
246
256
|
bash "$EVAL_DIR/integration/test_trust_checkpoint.sh" || result=1
|
|
@@ -53,4 +53,10 @@ require_codeowner "src/li""b/flow-resolver.ts" "src/li""b/flow-resolver.ts"
|
|
|
53
53
|
require_codeowner "src/cli/workflow-sidecar.ts" "src/cli/workflow-sidecar.ts"
|
|
54
54
|
require_codeowner "kits/*/flows/*.flow.json" "kits/*/flows/*.flow.json (kit FlowDefinitions)"
|
|
55
55
|
|
|
56
|
+
# ADR 0022 §2: delivery/DECLARED is the no-agent-delivery exemption marker
|
|
57
|
+
# scripts/ci/trust-reconcile.js reads — a self-asserted governance artifact (names
|
|
58
|
+
# approved_by) that must stay owner-review-gated the same as the other verify-config/
|
|
59
|
+
# anchor paths above (#301's file-level half; server-side review enforcement is #225).
|
|
60
|
+
require_codeowner "delivery/DECLARED" "delivery/DECLARED (ADR 0022 §2 no-agent-delivery marker)"
|
|
61
|
+
|
|
56
62
|
echo "FlowDefinition-related CODEOWNERS coverage checks passed."
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# test_knowledge_providers.sh — Layer 1: Knowledge Store Provider contract
|
|
3
|
+
# conformance + provider-agnostic health verbs (issue #317).
|
|
4
|
+
#
|
|
5
|
+
# Runs the node:test suites that back AC1–AC3:
|
|
6
|
+
# - providers/conformance/suite.test.js (AC3: all three providers conform)
|
|
7
|
+
# - providers/health/health-pass.test.js (AC1: vault+git-repo reports;
|
|
8
|
+
# AC2: work-item seeded dupe + broken blocker)
|
|
9
|
+
# Deterministic, dependency-free, fixture-driven — never touches a real board.
|
|
10
|
+
set -uo pipefail
|
|
11
|
+
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
12
|
+
cd "$ROOT_DIR"
|
|
13
|
+
|
|
14
|
+
echo "── Knowledge store provider conformance + health (node --test) ──"
|
|
15
|
+
|
|
16
|
+
if node --test \
|
|
17
|
+
kits/knowledge/providers/conformance/suite.test.js \
|
|
18
|
+
kits/knowledge/providers/health/health-pass.test.js; then
|
|
19
|
+
echo " PASS: knowledge store provider conformance + health verbs"
|
|
20
|
+
else
|
|
21
|
+
echo " FAIL: knowledge store provider conformance + health verbs"
|
|
22
|
+
exit 1
|
|
23
|
+
fi
|
|
@@ -20,6 +20,24 @@ Inherited from primitives:
|
|
|
20
20
|
| tool-verifier | verify-work |
|
|
21
21
|
| tool-playwright | 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`
|
|
27
|
+
(see `context/contracts/execution-contract.md` § Delegation: Model Routing —
|
|
28
|
+
that contract is the consumption instruction, `.datum/config.json` is the
|
|
29
|
+
source of truth for the mapping):
|
|
30
|
+
|
|
31
|
+
| Delegate | Role |
|
|
32
|
+
|---|---|
|
|
33
|
+
| tool-worker | `delegate-mechanical` for fully-specified mechanical tasks, `delegate-implementation` for precisely-planned implementation, `delegate-design` when the task needs design latitude |
|
|
34
|
+
| tool-planner | `delegate-design` |
|
|
35
|
+
| tool-code-reviewer / tool-security-reviewer | `delegate-implementation` |
|
|
36
|
+
| tool-verifier / tool-playwright | `delegate-implementation` |
|
|
37
|
+
|
|
38
|
+
If datum or the config is absent, fall back to the runtime's inherited model
|
|
39
|
+
and note the fallback in the session artifact.
|
|
40
|
+
|
|
23
41
|
## Orchestrator Rule
|
|
24
42
|
|
|
25
43
|
You never use `read`, `glob`, `grep`, or `code` on source files. You only read/write the session file and artifact files in `.kontourai/flow-agents/<slug>/`.
|
|
@@ -27,6 +45,7 @@ You never use `read`, `glob`, `grep`, or `code` on source files. You only read/w
|
|
|
27
45
|
## Shared Contracts
|
|
28
46
|
|
|
29
47
|
Follow:
|
|
48
|
+
- `context/contracts/standing-directives.md`
|
|
30
49
|
- `context/contracts/artifact-contract.md`
|
|
31
50
|
- `context/contracts/planning-contract.md`
|
|
32
51
|
- `context/contracts/execution-contract.md`
|
|
@@ -10,6 +10,8 @@ Select ready backlog work and prepare a bounded execution handoff without implem
|
|
|
10
10
|
## Contract
|
|
11
11
|
|
|
12
12
|
- Use `context/contracts/work-item-contract.md` as the source vocabulary for provider-backed work item shape, provider roles, and capability flags.
|
|
13
|
+
- Use `context/contracts/assignment-provider-contract.md` as the second source vocabulary for durable ownership: the `AssignmentProvider` operations (`claim`/`release`/`supersede`/`status`/`list`), the assignment ⋈ liveness join's effective-state enum (`held`/`reclaimable`/`human-held`/`free`), and the versioned claim-record format come from that contract, not from this skill.
|
|
14
|
+
- `pull-work` may perform exactly two provider-adjacent writes at selection: the liveness claim (ADR 0012, ephemeral, local runtime presence stream — see "1a. Liveness Selection Preflight" / "Liveness Claim On Selection") and the durable assignment claim (`context/contracts/assignment-provider-contract.md`, GitHub assignee/label/comment or a local-file record — see "Assignment Claim On Selection"); no other provider mutation is added to this skill.
|
|
13
15
|
- Read the configured backlog provider dynamically.
|
|
14
16
|
- Select one issue or a coherent issue group.
|
|
15
17
|
- Do not implement code.
|
|
@@ -37,9 +39,10 @@ Create or update `.kontourai/flow-agents/<slug>/<slug>--pull-work.md` with:
|
|
|
37
39
|
|
|
38
40
|
- `board_snapshot`: filters, issue list, labels, milestone/provider milestone state, project fields, state, blockers
|
|
39
41
|
- `wip_assessment`: active work, reviews, verification, CI remediation, with personal WIP separated from global conflict context
|
|
40
|
-
- `liveness_preflight`: per-candidate `{subjectId, state: held|reclaimable|free|mine, holder_actor?, status_raw}` plus `self_actor`/`self_actor_source`, computed via `liveness status --json` + `liveness whoami --json
|
|
42
|
+
- `liveness_preflight`: per-candidate `{subjectId, state: held|reclaimable|human-held|free|mine, holder_actor?, status_raw, effective_reason?}` plus `self_actor`/`self_actor_source`, computed via `liveness status --json` + `liveness whoami --json` joined with `assignment-provider status`'s `effective_state`/`reason` (see "1a. Liveness Selection Preflight"); `self_actor` is captured exactly once per pull-work pass and pinned for reuse (see "1a. Liveness Selection Preflight" and "Liveness Claim On Selection" — never re-derived per claim call); this is now the **full** ADR 0021 §1 `assignment ⋈ liveness` join (previously liveness-only pending #290 — #290 has landed)
|
|
41
43
|
- `reclaimable_override`: recorded only when a `reclaimable` (stale) candidate is selected, or when `--force`/an explicit user instruction overrides a `held` exclusion — the explicit opt-in decision and its stated reason (see "1a. Liveness Selection Preflight")
|
|
42
44
|
- `liveness_claim`: per selected item, `{subjectId, actor, emitted_at, ttl_seconds}`; on any claim-emit failure, `{skipped: <stderr reason>}` instead — fail-open (never block selection on a liveness-emit failure) but never silent: the skip reason is also surfaced in pull-work's user-facing output, and an unresolved-actor failure additionally names the remediation (`--actor <id>` / `FLOW_AGENTS_ACTOR=<id>` / a supported runtime), matching ADR 0012; also carries an optional `post_claim_conflict: {other_actor, detected_at}` when the post-claim re-check (see "Post-Claim Conflict Re-check") detects a double-hold
|
|
45
|
+
- `assignment_claim`: per selected item, `{provider: "github"|"local-file", subject_id, actor, command_evidence, confirmed_status}` — the durable-claim analog of `selected_item_ids`: provider kind, subject id, actor, the rendered/executed command evidence (`gh_commands`/`claim_comment_body` for `github`, the local-file `claim` invocation for `local-file`), and the confirmed `assignment-provider status` result for each selected item, recorded once the post-claim confirmation in "Assignment Claim On Selection" succeeds; see `context/contracts/assignment-provider-contract.md` for the underlying claim-record shape
|
|
43
46
|
- `my_active_work`: local worktrees, dirty branches, open PRs by the current user, active sidecars, and in-flight review/verification/release work owned by the current user
|
|
44
47
|
- `shepherding_candidates`: personal PRs, worktrees, or sidecars that should be reviewed, fixed, published, merged, abandoned, or cleaned before starting more work
|
|
45
48
|
- `stale_worktrees`: worktrees with no open PR, no recent activity, merged/abandoned branches, or unclear ownership that need an explicit keep/remove decision
|
|
@@ -136,18 +139,33 @@ For each ready candidate, derive `subjectId` via:
|
|
|
136
139
|
npm run workflow:sidecar -- resolve-slug <owner>/<repo>#<issue-number>
|
|
137
140
|
```
|
|
138
141
|
|
|
139
|
-
then group all rows for that `subjectId` by subject (classify per subject, not per row)
|
|
142
|
+
then group all rows for that `subjectId` by subject (classify per subject, not per row), reading each row's raw `status` field (never `label`). Detect the liveness-only double-hold case first, before computing the full assignment ⋈ liveness join below: when a `verified` row for an actor other than self and a `verified` row for the resolved self actor both exist on the same subject, that is a double-hold — surface it as `held` plus an explicit conflict warning (per ADR 0012 §4 detection: "a false-stale double-hold ... is detected ... not prevented"), never silently resolve it to `mine`.
|
|
140
143
|
|
|
141
|
-
|
|
142
|
-
2. else, a `verified` row for the resolved self actor ⇒ `mine`: hand to `### 2. Enforce WIP And Shepherding`'s existing personal-WIP logic; do not re-offer as new, do not exclude as held-by-other.
|
|
143
|
-
3. else, a `stale` row (any actor) ⇒ `reclaimable`: offered, flagged, with a warning; selecting it requires an explicit recorded opt-in (record in `reclaimable_override`, and/or `alignment_questions`), never a silent normal pick.
|
|
144
|
-
4. else (a `revoked`-only row, a `superseded` row, or no row at all) ⇒ `free`, offered normally — except a `superseded` row is surfaced but never auto-selected (full takeover semantics are #294; this is an accepted gap, not silently ignored).
|
|
144
|
+
Otherwise, compute the full ADR 0021 §1 `assignment ⋈ liveness` join per candidate via `assignment-provider status` (using the effective provider kind, repo, artifact root, `label_name`, and `claim_comment_marker` from `effective-assignment-provider-settings` — see "Assignment Claim On Selection" below — joined against the same liveness stream already read above):
|
|
145
145
|
|
|
146
|
-
|
|
146
|
+
```bash
|
|
147
|
+
npm run assignment-provider -- status \
|
|
148
|
+
--provider <github|local-file> \
|
|
149
|
+
--subject-id <subjectId> \
|
|
150
|
+
--liveness-stream <path-to-events.jsonl> \
|
|
151
|
+
--self-actor <self_actor>
|
|
152
|
+
# github: --issue-json <path-to-gh-issue-view-json|-> --label-name <label> --claim-comment-marker <marker>
|
|
153
|
+
# local-file: --artifact-root <dir>
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Read the returned `.effective.effective_state` and `.effective.reason` and classify in this precedence order:
|
|
157
|
+
|
|
158
|
+
1. `effective_state: "held"` with `reason: "self_is_holder"` ⇒ `mine`: hand to `### 2. Enforce WIP And Shepherding`'s existing personal-WIP logic; do not re-offer as new, do not exclude as held-by-other.
|
|
159
|
+
2. else `effective_state: "held"` (`reason: "fresh_liveness_heartbeat"` or `"liveness_claim_present_assignment_lagging"`) ⇒ excluded from the ready set by default.
|
|
160
|
+
3. else `effective_state: "reclaimable"` ⇒ offered, flagged, with a warning; selecting it requires an explicit recorded opt-in (record in `reclaimable_override`, and/or `alignment_questions`), never a silent normal pick. This issue does not implement takeover — offering a `reclaimable` candidate, never auto-reclaiming it, is the correct scope boundary (Design Decision 2; full takeover protocol is ADR 0021 §5 / #294).
|
|
161
|
+
4. else `effective_state: "human-held"` ⇒ surfaced, never auto-reclaimed (Design Decision 3 / ADR 0021 §6): record the assignee identity and idle duration (`effective.holder.assignee`, `effective.holder.idle_days`) in `alignment_questions` with a recommended answer (e.g. "assigned to `<assignee>`, idle `<idle_days>` days — reclaim?" recommending confirmation before proceeding), and select only on the user's explicit confirmation.
|
|
162
|
+
5. else `effective_state: "free"` ⇒ offered normally — except a `superseded` liveness row (no active assignment, no fresh liveness) is surfaced but never auto-selected (full takeover semantics are #294; this is an accepted gap, not silently ignored), exactly as before this join upgrade.
|
|
147
163
|
|
|
148
|
-
|
|
164
|
+
An explicit user instruction to proceed despite a `held` or `reclaimable` classification (`--force`, "take it anyway", equivalent) overrides the exclusion/opt-in requirement; the override and its stated reason must be recorded in the artifact (`liveness_preflight`, `reclaimable_override`, and/or `priority_rationale`). A `human-held` classification is never overridden by `--force` alone — only the user's explicit answer to the recorded `alignment_questions` entry authorizes selecting it.
|
|
149
165
|
|
|
150
|
-
|
|
166
|
+
This preflight now computes the **full** ADR 0021 §1 `assignment ⋈ liveness` join (previously liveness-only, pending #290): the assignment dimension is `#290`'s `AssignmentProvider` `status()` (`context/contracts/assignment-provider-contract.md`), joined against the same liveness stream this preflight already reads.
|
|
167
|
+
|
|
168
|
+
`liveness claim`/`status`/`whoami` read/write the local runtime liveness stream, never GitHub issue/label/assignee state — this is not a provider mutation, and the two-provider-writes-only invariant in `## Contract` is unchanged by this slice. `#290` adds the one narrow, audited durable assignment claim that pairs with this liveness emit — see "Assignment Claim On Selection" below.
|
|
151
169
|
|
|
152
170
|
### 2. Enforce WIP And Shepherding
|
|
153
171
|
|
|
@@ -260,7 +278,57 @@ Branch on the returned `winner.actor`:
|
|
|
260
278
|
- If `winner.actor !== self_actor`, this session is the loser: immediately run `npm run workflow:sidecar -- liveness release <subjectId> --actor <self_actor>`, extend `post_claim_conflict` with `{verdict_reason, winner_actor, conceded: true}`, and return to `### 3. Select Work` to reselect within the same `pull-work` pass — excluding the just-released subject — before any handoff to `plan-work`.
|
|
261
279
|
- If `winner.actor === self_actor`, this session wins: record the verdict for transparency (`{verdict_reason, winner_actor: self_actor, conceded: false}`) in `post_claim_conflict` and proceed normally; do not release.
|
|
262
280
|
|
|
263
|
-
Honesty note: this re-check narrows, but does not close, the read-then-write race between the preflight's read and the claim's write. The verdict-and-release loop above closes the "detected but advisory-only" gap ADR 0012 §4 names for THIS session's own double-hold — the loser deterministically concedes and re-selects within the same pass, a new convergence guarantee this slice adds — but it still does not provide true mutual exclusion across the read-then-write race window itself; that residual is unchanged from before.
|
|
281
|
+
Honesty note: this re-check narrows, but does not close, the read-then-write race between the preflight's read and the claim's write. The verdict-and-release loop above closes the "detected but advisory-only" gap ADR 0012 §4 names for THIS session's own double-hold — the loser deterministically concedes and re-selects within the same pass, a new convergence guarantee this slice adds — but it still does not provide true mutual exclusion across the read-then-write race window itself; that residual is unchanged from before. `#290`'s provider assignment lease closes this gap for the **local-file** provider only: `claim`/`release`/`supersede` there are wrapped in a per-subject `mkdir`-lock (atomic create, EEXIST-spin, staleness reclaim), so two concurrent local-file `claim` calls on the same subject genuinely cannot both win — true mutual exclusion, not just detection. The **GitHub** provider (assignee/label/claim-comment) remains advisory/last-writer: `render-claim`/`render-supersede` emit `gh` argv for the calling skill to run, and nothing about a GitHub issue's assignee/label/comment state gives atomic compare-and-swap across two concurrent skill invocations — the post-claim `status` re-check above still only *detects* a lost race after the fact, it does not *prevent* one. Do not read "provider assignment lease" as closing the GitHub race; only the `verify-hold` publish gate (#293), not yet implemented, would do that for GitHub.
|
|
282
|
+
|
|
283
|
+
### Assignment Claim On Selection
|
|
284
|
+
|
|
285
|
+
After the post-claim liveness conflict re-check above resolves (no double-hold detected, or this session won the deterministic tiebreaker and any loser has reselected) and `selected_item_ids` is otherwise ready to finalize, perform the durable assignment claim — the second and last provider-adjacent write this skill performs (see `## Contract`) — before recording `selected_item_ids` as final and before any handoff to `plan-work`.
|
|
286
|
+
|
|
287
|
+
Resolve the effective assignment provider settings once per pass:
|
|
288
|
+
|
|
289
|
+
```bash
|
|
290
|
+
npm run effective-assignment-provider-settings -- --repo-path . --json
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
If the result is `status: ask_user`, ask the user which `AssignmentProvider` (`github` or `local-file`) to use before claiming; do not silently assume. If `status: configured`, use the returned `settings.provider.kind`, `settings.provider.repo`, `settings.policy.label_name`, and `settings.policy.claim_comment_marker` for every call below.
|
|
294
|
+
|
|
295
|
+
For each selected item's `subjectId` (the same `subjectId` resolved via `resolve-slug` and used throughout this pass):
|
|
296
|
+
|
|
297
|
+
- **`kind: "github"`**: build an `--input-json` payload with `repo` (`{owner, name}`), `issue_number`, `assignee_login` (the current actor's GitHub login when known), `existing_assignee_login` and `existing_comment_id` (only when superseding an already-recorded `reclaimable`/confirmed `human-held` candidate — otherwise omit), `label_name`, `claim_comment_marker`, `ttl_seconds`, `branch`, and `artifact_dir`, then render:
|
|
298
|
+
|
|
299
|
+
```bash
|
|
300
|
+
npm run assignment-provider -- render-claim \
|
|
301
|
+
--provider github \
|
|
302
|
+
--subject-id <subjectId> \
|
|
303
|
+
--input-json <path> \
|
|
304
|
+
--actor-json <path>
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
Execute every command in the returned `gh_commands` array **verbatim** via the Bash tool, in order — never freehand `gh` text. Each `gh_commands` entry is an **argv array** (one element per argument, e.g. `["gh", "issue", "comment", "9101", "--repo", "kontourai/flow-agents", "--body", "..."]`) and MUST be executed as argv — every element passed as its own separate Bash-tool argument — **never** concatenated into a single shell-command string, and **never** run via `bash -c "..."` or any other shell re-interpretation of the joined elements. `claim_comment_body` (and, when superseding, `previous_record`) can carry attacker-influenced text (see the GitHub claim-record sanitization note in `context/contracts/assignment-provider-contract.md`); concatenating argv elements into a shell string before execution would reintroduce a shell-injection surface this render/execute split is designed to avoid. Record the rendered `record`, `claim_comment_body`, and each executed `gh_commands` entry as evidence.
|
|
308
|
+
|
|
309
|
+
- **`kind: "local-file"`**: call the local-file `claim` subcommand directly — no render step, since this is the one path that performs real I/O inside the CLI itself:
|
|
310
|
+
|
|
311
|
+
```bash
|
|
312
|
+
npm run assignment-provider -- claim \
|
|
313
|
+
--provider local-file \
|
|
314
|
+
--artifact-root <artifact-root> \
|
|
315
|
+
--subject-id <subjectId> \
|
|
316
|
+
--branch <branch> \
|
|
317
|
+
--artifact-dir <artifact_dir> \
|
|
318
|
+
--actor-json <path>
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
A non-zero exit (already claimed by a different actor) must not be silently retried or overwritten — treat it exactly like a `held`/`reclaimable` conflict and return to `### 3. Select Work` to reselect.
|
|
322
|
+
|
|
323
|
+
Then, regardless of provider kind, re-fetch current state — re-fetch the GitHub issue via `gh issue view --json assignees,labels,comments` for `github` (the local-file record is already current on disk) — and call `assignment-provider status` again, passing the same pinned `self_actor`, to **confirm** the claim landed before treating `selected_item_ids` as final:
|
|
324
|
+
|
|
325
|
+
```bash
|
|
326
|
+
npm run assignment-provider -- status --provider <github|local-file> --subject-id <subjectId> --self-actor <self_actor> ...
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
Confirm `effective.effective_state === "held"` with `effective.reason === "self_is_holder"` (the join function's signal that this session's own actor is the confirmed holder). Only after this confirmation does `selected_item_ids` become final for handoff. If confirmation fails — the claim did not land, or a different actor's record now appears — treat this exactly like a liveness double-hold: do not proceed to `plan-work` with this subject; return to `### 3. Select Work` to reselect, excluding the contested subject.
|
|
330
|
+
|
|
331
|
+
Record the render/status evidence — provider kind, subject id, actor, the rendered/executed command(s), and the confirmed `status()` result — for every selected item under `selection` and in the dedicated `assignment_claim` artifact field (see Artifact Contract above).
|
|
264
332
|
|
|
265
333
|
### 4. Anchor Check
|
|
266
334
|
|
package/kits/knowledge/kit.json
CHANGED
|
@@ -91,6 +91,31 @@
|
|
|
91
91
|
"id": "knowledge.obsidian-store",
|
|
92
92
|
"path": "adapters/obsidian-store/index.js",
|
|
93
93
|
"description": "Obsidian store adapter: each record is one human-canonical Obsidian markdown note. Frontmatter carries all contract fields; body rendered with callouts (raw), readable prose + Sources/Related wikilink sections (compiled/concept/snapshot). Category dots map to folder hierarchy; title-slugified filenames with collision suffix; superseded records move to archive/ (supersede-not-delete invariant). Backed by shared codec with default-store."
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"id": "knowledge.markdown-vault-provider",
|
|
97
|
+
"path": "providers/markdown-vault/index.js",
|
|
98
|
+
"description": "Knowledge store PROVIDER (issue #317): adapts the markdown-vault store behind the typed graph model (nodes/edges/provenance). Thin read + proposals-only wrapper over an existing store adapter; existing skills unaffected."
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"id": "knowledge.git-repo-provider",
|
|
102
|
+
"path": "providers/git-repo/index.js",
|
|
103
|
+
"description": "Knowledge store PROVIDER (issue #317): reads git-tracked docs (docs/decisions/* tombstones+evidence, CONTEXT.md glossary, docs/learnings/*) as decision/note nodes with supersedes/merged-into/evidence-of edges. Write side = decision-topic proposals shaped for the promote sub-flow."
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
"id": "knowledge.work-item-provider",
|
|
107
|
+
"path": "providers/work-item/index.js",
|
|
108
|
+
"description": "Knowledge store PROVIDER/source-sink adapter (issue #317): maps GitHub issues to issue nodes and flow-agents:work-item-metadata blockers + prose refs to blocks/relates edges via an injectable gh runner. Write side = draft comment/label proposals. Enables backlog hygiene as a knowledge-health pass."
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"id": "knowledge.health-verbs",
|
|
112
|
+
"path": "providers/health/index.js",
|
|
113
|
+
"description": "Provider-agnostic knowledge-health verbs (issue #317): detectDuplicates + checkDependencyLinkIntegrity over any provider's graph, returning schema-valid health reports (schemas/knowledge/health-report.schema.json)."
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"id": "knowledge.store-providers",
|
|
117
|
+
"path": "providers/index.js",
|
|
118
|
+
"description": "Public entry point for the knowledge store providers, health verbs, graph model helpers, and schema validator. See context/contracts/knowledge-store-contract.md."
|
|
94
119
|
}
|
|
95
120
|
],
|
|
96
121
|
"evals": [
|
|
@@ -153,6 +178,16 @@
|
|
|
153
178
|
"id": "knowledge.hygiene-review-suite",
|
|
154
179
|
"path": "evals/hygiene-review/suite.test.js",
|
|
155
180
|
"description": "Eval cases for hygiene-review (#106 hygiene #5, closes the issue): opt-in orchestration (omitted audit block is skipped; empty review does nothing); each opted-in audit runs via its existing flow-runner method and its findings are collected verbatim (no detection reimplemented); proposals are normalized to adopt/retire/merge and each cites its sourceFlow + evidence; thin-orchestrator/consume-never-fork invariants — read-only by default mutates no record, no new gate is forked, the only mutation (glossary apply:true) is delegated to glossarySync's own gated propose→apply (verified via proposes-link + mutation-log on the resulting concept); sub-flow telemetry is folded in plus hygiene-review's own orchestrate-gate + review-gate; module-level export delegates to the runner."
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
"id": "knowledge.provider-conformance-suite",
|
|
184
|
+
"path": "providers/conformance/suite.test.js",
|
|
185
|
+
"description": "Conformance suite (issue #317, AC3): parameterized suite every knowledge store provider (markdown-vault, git-repo, work-item) must pass — read interface returns schema-valid nodes/edges with provenance, closed edge vocabulary, queryByType subset consistency, and proposeWrite returns a schema-valid 'proposed' proposal that mutates nothing (proposals-only)."
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
"id": "knowledge.provider-health-pass-suite",
|
|
189
|
+
"path": "providers/health/health-pass.test.js",
|
|
190
|
+
"description": "Health-pass suite (issue #317, AC1+AC2): the same health command over vault + git-repo providers yields schema-valid reports (AC1); a backlog pass over the work-item adapter driven by a seeded fixture flags a duplicate and a broken blocker link (AC2) — never the real board."
|
|
156
191
|
}
|
|
157
192
|
],
|
|
158
193
|
"skills": [
|
package/kits/knowledge/providers/conformance/fixtures/git-repo/docs/decisions/old-sprocket-shape.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
status: merged
|
|
3
|
+
subject: Old sprocket shape
|
|
4
|
+
decided: 2026-07-02
|
|
5
|
+
merged_into: sprocket-shape
|
|
6
|
+
evidence:
|
|
7
|
+
- kind: pr
|
|
8
|
+
ref: https://github.com/kontourai/flow-agents/pull/319
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Old sprocket shape
|
|
12
|
+
|
|
13
|
+
Merged into [sprocket-shape](./sprocket-shape.md).
|
package/kits/knowledge/providers/conformance/fixtures/git-repo/docs/decisions/sprocket-shape.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
status: current
|
|
3
|
+
subject: Sprocket shape
|
|
4
|
+
decided: 2026-07-02
|
|
5
|
+
supersedes:
|
|
6
|
+
- old-sprocket-shape
|
|
7
|
+
evidence:
|
|
8
|
+
- kind: pr
|
|
9
|
+
ref: https://github.com/kontourai/flow-agents/pull/319
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Sprocket shape
|
|
13
|
+
|
|
14
|
+
Sprockets are round. Absorbs the old-sprocket-shape topic.
|
package/kits/knowledge/providers/conformance/fixtures/git-repo/docs/decisions/widget-format.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
status: current
|
|
3
|
+
subject: Widget format
|
|
4
|
+
decided: 2026-07-01
|
|
5
|
+
evidence:
|
|
6
|
+
- kind: issue
|
|
7
|
+
ref: https://github.com/kontourai/flow-agents/issues/317
|
|
8
|
+
- kind: doc
|
|
9
|
+
ref: docs/learnings/fixture-learning.md
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Widget format
|
|
13
|
+
|
|
14
|
+
Widgets are stored as JSON. Lean rationale linked via evidence.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"number": 14,
|
|
4
|
+
"title": "Add dark mode toggle to settings",
|
|
5
|
+
"state": "OPEN",
|
|
6
|
+
"labels": [{ "name": "enhancement" }],
|
|
7
|
+
"body": "Users want a dark mode toggle in the settings screen.\n\n<!-- flow-agents:work-item-metadata\n{\n \"schema_version\": \"1.0\",\n \"blockers\": []\n}\n-->"
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"number": 15,
|
|
11
|
+
"title": "Add dark-mode toggle to settings page",
|
|
12
|
+
"state": "OPEN",
|
|
13
|
+
"labels": [{ "name": "enhancement" }],
|
|
14
|
+
"body": "Duplicate of the dark mode toggle request. Settings page needs a dark-mode toggle."
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"number": 20,
|
|
18
|
+
"title": "Ship theme system",
|
|
19
|
+
"state": "OPEN",
|
|
20
|
+
"labels": [{ "name": "feature" }],
|
|
21
|
+
"body": "Overarching theme work. Relates to #14.\n\n<!-- flow-agents:work-item-metadata\n{\n \"schema_version\": \"1.0\",\n \"blockers\": [\n { \"repo\": \"seed/fixture\", \"number\": 14 },\n { \"repo\": \"seed/fixture\", \"number\": 999 }\n ]\n}\n-->"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"number": 21,
|
|
25
|
+
"title": "Document theme tokens",
|
|
26
|
+
"state": "OPEN",
|
|
27
|
+
"labels": [],
|
|
28
|
+
"body": "Blocked by #20. Also see #14 for the toggle."
|
|
29
|
+
}
|
|
30
|
+
]
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Knowledge Store Provider — Conformance Suite (issue #317, AC3).
|
|
3
|
+
*
|
|
4
|
+
* One parameterized suite every provider must pass. Proves that markdown-vault,
|
|
5
|
+
* git-repo, and work-item all satisfy the read + proposals-only interface and
|
|
6
|
+
* emit schema-valid nodes/edges/proposals. Run:
|
|
7
|
+
* node --test kits/knowledge/providers/conformance/suite.test.js
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { test, describe, before, after } from "node:test";
|
|
11
|
+
import assert from "node:assert/strict";
|
|
12
|
+
import * as fs from "node:fs";
|
|
13
|
+
import * as os from "node:os";
|
|
14
|
+
import * as path from "node:path";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
|
|
17
|
+
import { MarkdownVaultProvider } from "../markdown-vault/index.js";
|
|
18
|
+
import { GitRepoProvider } from "../git-repo/index.js";
|
|
19
|
+
import { WorkItemProvider } from "../work-item/index.js";
|
|
20
|
+
import DefaultKnowledgeStore from "../../adapters/default-store/index.js";
|
|
21
|
+
import { loadSchemas, EDGE_TYPES } from "../lib/model.js";
|
|
22
|
+
import { validate } from "../lib/schema-validate.js";
|
|
23
|
+
|
|
24
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
25
|
+
const FIXTURES = path.join(__dirname, "fixtures");
|
|
26
|
+
const schemas = loadSchemas();
|
|
27
|
+
|
|
28
|
+
function assertSchema(value, schema, label) {
|
|
29
|
+
const { valid, errors } = validate(value, schema);
|
|
30
|
+
assert.ok(valid, `${label} not schema-valid:\n ${errors.join("\n ")}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// --- per-provider fixture builders --------------------------------------
|
|
34
|
+
|
|
35
|
+
async function buildVault() {
|
|
36
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "kg-vault-conf-"));
|
|
37
|
+
const store = new DefaultKnowledgeStore({ storeRoot: dir });
|
|
38
|
+
const rawId = await store.create({ type: "raw", title: "Raw source", body: "raw material", category: "eng", provenance: { agent: "test" } });
|
|
39
|
+
const compiledId = await store.create({ type: "compiled", title: "Compiled note", body: "distilled", category: "eng", provenance: { agent: "test" } });
|
|
40
|
+
await store.link(compiledId, [{ target_id: rawId, kind: "source" }], { agent: "test" });
|
|
41
|
+
const provider = new MarkdownVaultProvider({ store, storeRoot: dir, agent: "test" });
|
|
42
|
+
return { provider, proposeIntent: { title: "Proposed note", body: "content", category: "eng" }, cleanup: () => fs.rmSync(dir, { recursive: true, force: true }) };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function buildGitRepo() {
|
|
46
|
+
const provider = new GitRepoProvider({ repoRoot: path.join(FIXTURES, "git-repo"), agent: "test" });
|
|
47
|
+
return { provider, proposeIntent: { subject: "Fixture decision", body: "the answer", evidence: [{ kind: "issue", ref: "#317" }] }, cleanup: () => {} };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function buildWorkItem() {
|
|
51
|
+
const issues = fs.readFileSync(path.join(FIXTURES, "work-item", "issues.json"), "utf8");
|
|
52
|
+
const runner = async () => issues;
|
|
53
|
+
const provider = new WorkItemProvider({ repo: "seed/fixture", runner, agent: "test" });
|
|
54
|
+
return { provider, proposeIntent: { issue: 14, commentBody: "draft comment" }, cleanup: () => {} };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const PROVIDERS = [
|
|
58
|
+
{ name: "markdown-vault", build: buildVault, expectedId: "markdown-vault" },
|
|
59
|
+
{ name: "git-repo", build: buildGitRepo, expectedId: "git-repo" },
|
|
60
|
+
{ name: "work-item", build: buildWorkItem, expectedId: "work-item" },
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
for (const { name, build, expectedId } of PROVIDERS) {
|
|
64
|
+
describe(`conformance: ${name}`, () => {
|
|
65
|
+
let ctx;
|
|
66
|
+
before(async () => { ctx = await build(); });
|
|
67
|
+
after(() => ctx.cleanup());
|
|
68
|
+
|
|
69
|
+
test("capabilities() declares proposals-only, non-writable", () => {
|
|
70
|
+
const cap = ctx.provider.capabilities();
|
|
71
|
+
assert.equal(cap.id, expectedId);
|
|
72
|
+
assert.equal(cap.writable, false);
|
|
73
|
+
assert.equal(cap.write_mode, "proposals-only");
|
|
74
|
+
assert.ok(Array.isArray(cap.proposal_targets) && cap.proposal_targets.length > 0);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("readNodes() returns schema-valid nodes with unique ids and provider provenance", async () => {
|
|
78
|
+
const nodes = await ctx.provider.readNodes();
|
|
79
|
+
assert.ok(Array.isArray(nodes) && nodes.length > 0, "expected at least one node");
|
|
80
|
+
const ids = new Set();
|
|
81
|
+
for (const n of nodes) {
|
|
82
|
+
assertSchema(n, schemas.node, `${name} node ${n.id}`);
|
|
83
|
+
assert.equal(n.provenance.provider, expectedId);
|
|
84
|
+
assert.ok(!ids.has(n.id), `duplicate node id ${n.id}`);
|
|
85
|
+
ids.add(n.id);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("readEdges() returns schema-valid edges in the closed edge vocabulary", async () => {
|
|
90
|
+
const edges = await ctx.provider.readEdges();
|
|
91
|
+
assert.ok(Array.isArray(edges));
|
|
92
|
+
for (const e of edges) {
|
|
93
|
+
assertSchema(e, schemas.edge, `${name} edge ${e.id}`);
|
|
94
|
+
assert.ok(EDGE_TYPES.includes(e.type), `edge type ${e.type} outside closed vocabulary`);
|
|
95
|
+
assert.equal(e.provenance.provider, expectedId);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("queryByType() is a consistent subset of readNodes()", async () => {
|
|
100
|
+
const nodes = await ctx.provider.readNodes();
|
|
101
|
+
const types = [...new Set(nodes.map((n) => n.type))];
|
|
102
|
+
for (const t of types) {
|
|
103
|
+
const q = await ctx.provider.queryByType(t);
|
|
104
|
+
assert.ok(q.every((n) => n.type === t), `queryByType(${t}) returned off-type node`);
|
|
105
|
+
assert.equal(q.length, nodes.filter((n) => n.type === t).length);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("readGraph() returns { nodes, edges }", async () => {
|
|
110
|
+
const g = await ctx.provider.readGraph();
|
|
111
|
+
assert.ok(Array.isArray(g.nodes) && Array.isArray(g.edges));
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("proposeWrite() returns a schema-valid 'proposed' proposal and mutates nothing", async () => {
|
|
115
|
+
const before = await ctx.provider.readNodes();
|
|
116
|
+
const p = await ctx.provider.proposeWrite(ctx.proposeIntent);
|
|
117
|
+
assertSchema(p, schemas.proposal, `${name} proposal`);
|
|
118
|
+
assert.equal(p.status, "proposed");
|
|
119
|
+
assert.equal(p.provider, expectedId);
|
|
120
|
+
assert.equal(typeof p.rendered, "string");
|
|
121
|
+
const after = await ctx.provider.readNodes();
|
|
122
|
+
assert.equal(after.length, before.length, "proposeWrite must not add nodes to the store");
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
}
|