@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
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal, dependency-free JSON-Schema validator.
|
|
3
|
+
*
|
|
4
|
+
* Supports exactly the draft-07 subset used by the Knowledge Kit graph schemas
|
|
5
|
+
* (schemas/knowledge/*.schema.json): type, required, properties,
|
|
6
|
+
* additionalProperties:false, enum, const, items, minLength, minItems, integer,
|
|
7
|
+
* and local `$ref` into `#/$defs/*`. It is intentionally NOT a general validator
|
|
8
|
+
* — it exists so health reports and conformance tests can assert schema validity
|
|
9
|
+
* without pulling an external dependency (the same zero-dep discipline the
|
|
10
|
+
* decision-registry validator follows).
|
|
11
|
+
*
|
|
12
|
+
* @module providers/lib/schema-validate
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
function typeOf(value) {
|
|
16
|
+
if (value === null) return "null";
|
|
17
|
+
if (Array.isArray(value)) return "array";
|
|
18
|
+
return typeof value;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function matchesType(value, type) {
|
|
22
|
+
if (type === "integer") return typeof value === "number" && Number.isInteger(value);
|
|
23
|
+
if (type === "number") return typeof value === "number";
|
|
24
|
+
if (type === "object") return typeOf(value) === "object";
|
|
25
|
+
if (type === "array") return Array.isArray(value);
|
|
26
|
+
return typeOf(value) === type;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function resolveRef(ref, root) {
|
|
30
|
+
if (!ref.startsWith("#/")) throw new Error(`Unsupported $ref (only local #/ refs): ${ref}`);
|
|
31
|
+
const parts = ref.slice(2).split("/");
|
|
32
|
+
let node = root;
|
|
33
|
+
for (const part of parts) {
|
|
34
|
+
node = node && node[part];
|
|
35
|
+
}
|
|
36
|
+
if (!node) throw new Error(`Unresolved $ref: ${ref}`);
|
|
37
|
+
return node;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function validateNode(value, schema, root, pathStr, errors) {
|
|
41
|
+
if (schema.$ref) {
|
|
42
|
+
return validateNode(value, resolveRef(schema.$ref, root), root, pathStr, errors);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if ("const" in schema) {
|
|
46
|
+
if (value !== schema.const) {
|
|
47
|
+
errors.push(`${pathStr}: expected const ${JSON.stringify(schema.const)}, got ${JSON.stringify(value)}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (schema.enum) {
|
|
52
|
+
if (!schema.enum.includes(value)) {
|
|
53
|
+
errors.push(`${pathStr}: value ${JSON.stringify(value)} not in enum ${JSON.stringify(schema.enum)}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (schema.type) {
|
|
58
|
+
const types = Array.isArray(schema.type) ? schema.type : [schema.type];
|
|
59
|
+
if (!types.some((t) => matchesType(value, t))) {
|
|
60
|
+
errors.push(`${pathStr}: expected type ${types.join("|")}, got ${typeOf(value)}`);
|
|
61
|
+
return; // further checks assume the type held
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (typeof value === "string") {
|
|
66
|
+
if (typeof schema.minLength === "number" && value.length < schema.minLength) {
|
|
67
|
+
errors.push(`${pathStr}: string shorter than minLength ${schema.minLength}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (Array.isArray(value)) {
|
|
72
|
+
if (typeof schema.minItems === "number" && value.length < schema.minItems) {
|
|
73
|
+
errors.push(`${pathStr}: array shorter than minItems ${schema.minItems}`);
|
|
74
|
+
}
|
|
75
|
+
if (schema.items) {
|
|
76
|
+
value.forEach((item, i) => validateNode(item, schema.items, root, `${pathStr}[${i}]`, errors));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (typeOf(value) === "object") {
|
|
81
|
+
const props = schema.properties || {};
|
|
82
|
+
for (const req of schema.required || []) {
|
|
83
|
+
if (!(req in value)) errors.push(`${pathStr}: missing required property '${req}'`);
|
|
84
|
+
}
|
|
85
|
+
if (schema.additionalProperties === false) {
|
|
86
|
+
for (const key of Object.keys(value)) {
|
|
87
|
+
if (!(key in props)) errors.push(`${pathStr}: unexpected property '${key}'`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
for (const [key, propSchema] of Object.entries(props)) {
|
|
91
|
+
if (key in value) {
|
|
92
|
+
validateNode(value[key], propSchema, root, `${pathStr}/${key}`, errors);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Validate `value` against `schema`.
|
|
100
|
+
* @returns {{ valid: boolean, errors: string[] }}
|
|
101
|
+
*/
|
|
102
|
+
export function validate(value, schema) {
|
|
103
|
+
const errors = [];
|
|
104
|
+
validateNode(value, schema, schema, "$", errors);
|
|
105
|
+
return { valid: errors.length === 0, errors };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Assert validity, throwing a descriptive error when invalid. Handy in tests.
|
|
110
|
+
*/
|
|
111
|
+
export function assertValid(value, schema, label = "value") {
|
|
112
|
+
const { valid, errors } = validate(value, schema);
|
|
113
|
+
if (!valid) {
|
|
114
|
+
const err = new Error(`${label} failed schema validation:\n ${errors.join("\n ")}`);
|
|
115
|
+
err.code = "SCHEMA_INVALID";
|
|
116
|
+
err.errors = errors;
|
|
117
|
+
throw err;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* markdown-vault knowledge-store provider.
|
|
3
|
+
*
|
|
4
|
+
* Adapts the existing Knowledge Kit store (the markdown-vault behaviour:
|
|
5
|
+
* markdown files + YAML frontmatter + [[wikilinks]] + a JSON graph index) to
|
|
6
|
+
* the knowledge-store PROVIDER contract (typed nodes/edges/provenance). It is a
|
|
7
|
+
* thin READ + PROPOSE wrapper over an existing store adapter instance — it does
|
|
8
|
+
* NOT reimplement or modify the adapter, so existing Knowledge Kit skills are
|
|
9
|
+
* unaffected. Wikilinks/frontmatter remain the native form; proposeWrite renders
|
|
10
|
+
* a new note in exactly that form for a human to file.
|
|
11
|
+
*
|
|
12
|
+
* @module providers/markdown-vault
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { node, edge, proposal, provenance } from "../lib/model.js";
|
|
16
|
+
import DefaultKnowledgeStore from "../../adapters/default-store/index.js";
|
|
17
|
+
|
|
18
|
+
const PROVIDER_ID = "markdown-vault";
|
|
19
|
+
|
|
20
|
+
/** Vault record type -> graph node type. Extensible: unknowns fall back to note. */
|
|
21
|
+
function nodeTypeFor(recordType) {
|
|
22
|
+
return recordType === "person" ? "person" : "note";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Vault link kind -> closed graph edge type. */
|
|
26
|
+
function edgeTypeFor(linkKind) {
|
|
27
|
+
switch (linkKind) {
|
|
28
|
+
case "supersedes":
|
|
29
|
+
case "refines":
|
|
30
|
+
return "supersedes";
|
|
31
|
+
case "source":
|
|
32
|
+
case "example":
|
|
33
|
+
return "evidence-of";
|
|
34
|
+
case "appears-in":
|
|
35
|
+
case "person":
|
|
36
|
+
return "mentions";
|
|
37
|
+
case "proposes":
|
|
38
|
+
case "related":
|
|
39
|
+
default:
|
|
40
|
+
return "relates";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const ALL_RECORD_TYPES = ["raw", "compiled", "concept", "snapshot", "person"];
|
|
45
|
+
|
|
46
|
+
export class MarkdownVaultProvider {
|
|
47
|
+
constructor({ store, storeRoot, agent } = {}) {
|
|
48
|
+
if (!store && !storeRoot) {
|
|
49
|
+
throw new Error("MarkdownVaultProvider requires { store } or { storeRoot }");
|
|
50
|
+
}
|
|
51
|
+
this.store = store || new DefaultKnowledgeStore({ storeRoot });
|
|
52
|
+
this.storeRoot = storeRoot || "(injected store)";
|
|
53
|
+
this.agent = agent;
|
|
54
|
+
this.id = PROVIDER_ID;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
capabilities() {
|
|
58
|
+
return {
|
|
59
|
+
id: PROVIDER_ID,
|
|
60
|
+
node_types: ["note", "person"],
|
|
61
|
+
edge_types: ["supersedes", "evidence-of", "mentions", "relates"],
|
|
62
|
+
writable: false,
|
|
63
|
+
write_mode: "proposals-only",
|
|
64
|
+
proposal_targets: ["create-node"],
|
|
65
|
+
source_of_truth: "human-curated markdown vault",
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async _allRecords() {
|
|
70
|
+
const out = [];
|
|
71
|
+
for (const t of ALL_RECORD_TYPES) {
|
|
72
|
+
const recs = await this.store.listByType(t, { includeRetired: true });
|
|
73
|
+
for (const r of recs) out.push(r);
|
|
74
|
+
}
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async readNodes(options = {}) {
|
|
79
|
+
const records = await this._allRecords();
|
|
80
|
+
const nodes = records.map((r) => {
|
|
81
|
+
const attributes = { record_type: r.type };
|
|
82
|
+
if (r.category) attributes.category = r.category;
|
|
83
|
+
if (r.status) attributes.status = r.status;
|
|
84
|
+
if (Array.isArray(r.tags) && r.tags.length) attributes.tags = r.tags;
|
|
85
|
+
return node({
|
|
86
|
+
id: r.id,
|
|
87
|
+
type: nodeTypeFor(r.type),
|
|
88
|
+
title: r.title,
|
|
89
|
+
body: typeof r.body === "string" ? r.body : "",
|
|
90
|
+
attributes,
|
|
91
|
+
provenance: provenance({
|
|
92
|
+
provider: PROVIDER_ID,
|
|
93
|
+
source: `${this.storeRoot}#${r.id}`,
|
|
94
|
+
locator: r.type,
|
|
95
|
+
agent: this.agent,
|
|
96
|
+
}),
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
if (options.type) return nodes.filter((n) => n.type === options.type);
|
|
100
|
+
return nodes;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async readEdges() {
|
|
104
|
+
const records = await this._allRecords();
|
|
105
|
+
const edges = [];
|
|
106
|
+
for (const r of records) {
|
|
107
|
+
const links = await this.store.getLinks(r.id);
|
|
108
|
+
for (const l of links.forward || []) {
|
|
109
|
+
edges.push(
|
|
110
|
+
edge({
|
|
111
|
+
id: `${r.id}--${l.kind}--${l.target_id}`,
|
|
112
|
+
type: edgeTypeFor(l.kind),
|
|
113
|
+
from: r.id,
|
|
114
|
+
to: l.target_id,
|
|
115
|
+
attributes: { vault_link_kind: l.kind },
|
|
116
|
+
provenance: provenance({
|
|
117
|
+
provider: PROVIDER_ID,
|
|
118
|
+
source: `${this.storeRoot}#${r.id}`,
|
|
119
|
+
locator: `links.${l.kind}`,
|
|
120
|
+
agent: this.agent,
|
|
121
|
+
}),
|
|
122
|
+
}),
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return edges;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async queryByType(type) {
|
|
130
|
+
return this.readNodes({ type });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async readGraph() {
|
|
134
|
+
const [nodes, edges] = await Promise.all([this.readNodes(), this.readEdges()]);
|
|
135
|
+
return { nodes, edges };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Propose a new vault note. Renders the native markdown-vault form
|
|
140
|
+
* (frontmatter + [[wikilinks]]) WITHOUT touching the store — proposals only.
|
|
141
|
+
*/
|
|
142
|
+
async proposeWrite(intent = {}) {
|
|
143
|
+
const { title, body = "", category = "inbox", tags = [], links = [] } = intent;
|
|
144
|
+
if (!title) throw new Error("markdown-vault proposeWrite requires intent.title");
|
|
145
|
+
const wikilinks = links.map((l) => `[[${l.target_id}${l.label ? `|${l.label}` : ""}]]`).join(" ");
|
|
146
|
+
const fm = [
|
|
147
|
+
"---",
|
|
148
|
+
"type: compiled",
|
|
149
|
+
`title: ${title}`,
|
|
150
|
+
`category: ${category}`,
|
|
151
|
+
tags.length ? `tags: [${tags.join(", ")}]` : "tags: []",
|
|
152
|
+
"---",
|
|
153
|
+
"",
|
|
154
|
+
body,
|
|
155
|
+
wikilinks ? `\n${wikilinks}` : "",
|
|
156
|
+
].join("\n");
|
|
157
|
+
return proposal({
|
|
158
|
+
provider: PROVIDER_ID,
|
|
159
|
+
kind: "create-node",
|
|
160
|
+
target: { store_root: this.storeRoot, category },
|
|
161
|
+
payload: { type: "note", title, body, category, tags, links },
|
|
162
|
+
rendered: fm,
|
|
163
|
+
rationale: intent.rationale || "Proposed vault note (file it via the Knowledge Kit ingest/compile flow).",
|
|
164
|
+
provenance: provenance({ provider: PROVIDER_ID, source: this.storeRoot, agent: this.agent }),
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export default MarkdownVaultProvider;
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* work-item knowledge-store provider (source/sink adapter, not storage).
|
|
3
|
+
*
|
|
4
|
+
* Maps a GitHub issue backlog into the typed graph so backlog hygiene becomes a
|
|
5
|
+
* knowledge-health pass:
|
|
6
|
+
* - issues -> `issue` nodes (state / labels / title).
|
|
7
|
+
* - flow-agents:work-item-metadata `blockers` -> `blocks` edges.
|
|
8
|
+
* - prose "blocked by #N" -> `blocks` edges; other #N refs -> `relates`.
|
|
9
|
+
*
|
|
10
|
+
* Reads via an INJECTABLE runner so tests drive it from recorded fixtures and
|
|
11
|
+
* never touch the real board. Write side is proposals-only: proposeWrite renders
|
|
12
|
+
* a draft issue comment / label change for an operator to file — it never calls
|
|
13
|
+
* a mutating gh command.
|
|
14
|
+
*
|
|
15
|
+
* @module providers/work-item
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { execFile } from "node:child_process";
|
|
19
|
+
import { node, edge, proposal, provenance } from "../lib/model.js";
|
|
20
|
+
|
|
21
|
+
const PROVIDER_ID = "work-item";
|
|
22
|
+
|
|
23
|
+
/** Default runner: shells out to `gh` (read-only list). */
|
|
24
|
+
function defaultRunner(args) {
|
|
25
|
+
return new Promise((resolve, reject) => {
|
|
26
|
+
execFile("gh", args, { maxBuffer: 32 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
27
|
+
if (err) return reject(new Error(`gh ${args.join(" ")} failed: ${stderr || err.message}`));
|
|
28
|
+
resolve(stdout);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Parse the flow-agents:work-item-metadata JSON block from an issue body. */
|
|
34
|
+
export function parseWorkItemMetadata(body) {
|
|
35
|
+
if (!body) return null;
|
|
36
|
+
const start = body.indexOf("flow-agents:work-item-metadata");
|
|
37
|
+
if (start === -1) return null;
|
|
38
|
+
const jsonStart = body.indexOf("{", start);
|
|
39
|
+
const end = body.indexOf("-->", jsonStart);
|
|
40
|
+
if (jsonStart === -1 || end === -1) return null;
|
|
41
|
+
try {
|
|
42
|
+
return JSON.parse(body.slice(jsonStart, end).trim());
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Extract a blocker issue number from a metadata blocker entry. */
|
|
49
|
+
function blockerNumber(entry) {
|
|
50
|
+
if (typeof entry === "number") return entry;
|
|
51
|
+
if (typeof entry === "string") {
|
|
52
|
+
const m = entry.match(/#?(\d+)/);
|
|
53
|
+
return m ? Number(m[1]) : null;
|
|
54
|
+
}
|
|
55
|
+
if (entry && typeof entry === "object") {
|
|
56
|
+
if (typeof entry.number === "number") return entry.number;
|
|
57
|
+
if (typeof entry.issue === "number") return entry.issue;
|
|
58
|
+
if (typeof entry.ref === "string") {
|
|
59
|
+
const m = entry.ref.match(/#?(\d+)/);
|
|
60
|
+
return m ? Number(m[1]) : null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export class WorkItemProvider {
|
|
67
|
+
constructor({ repo, runner, agent } = {}) {
|
|
68
|
+
this.repo = repo || "";
|
|
69
|
+
this.runner = runner || defaultRunner;
|
|
70
|
+
this.agent = agent;
|
|
71
|
+
this.id = PROVIDER_ID;
|
|
72
|
+
this._cache = null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
capabilities() {
|
|
76
|
+
return {
|
|
77
|
+
id: PROVIDER_ID,
|
|
78
|
+
node_types: ["issue"],
|
|
79
|
+
edge_types: ["blocks", "relates"],
|
|
80
|
+
writable: false,
|
|
81
|
+
write_mode: "proposals-only",
|
|
82
|
+
proposal_targets: ["comment", "label"],
|
|
83
|
+
source_of_truth: "GitHub issues (read via gh; write = draft comments/labels)",
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async _issues() {
|
|
88
|
+
if (this._cache) return this._cache;
|
|
89
|
+
const args = ["issue", "list"];
|
|
90
|
+
if (this.repo) args.push("--repo", this.repo);
|
|
91
|
+
args.push("--state", "all", "--json", "number,title,state,labels,body", "--limit", "200");
|
|
92
|
+
const out = await this.runner(args);
|
|
93
|
+
const parsed = JSON.parse(out);
|
|
94
|
+
this._cache = Array.isArray(parsed) ? parsed : [];
|
|
95
|
+
return this._cache;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async readNodes(options = {}) {
|
|
99
|
+
const issues = await this._issues();
|
|
100
|
+
const now = new Date().toISOString();
|
|
101
|
+
const nodes = issues.map((iss) =>
|
|
102
|
+
node({
|
|
103
|
+
id: `issue:${iss.number}`,
|
|
104
|
+
type: "issue",
|
|
105
|
+
title: iss.title || `issue #${iss.number}`,
|
|
106
|
+
body: iss.body || "",
|
|
107
|
+
attributes: {
|
|
108
|
+
number: iss.number,
|
|
109
|
+
state: iss.state || "OPEN",
|
|
110
|
+
labels: (iss.labels || []).map((l) => (typeof l === "string" ? l : l.name)),
|
|
111
|
+
},
|
|
112
|
+
provenance: provenance({
|
|
113
|
+
provider: PROVIDER_ID,
|
|
114
|
+
source: this.repo ? `${this.repo}#${iss.number}` : `#${iss.number}`,
|
|
115
|
+
retrievedAt: now,
|
|
116
|
+
agent: this.agent,
|
|
117
|
+
}),
|
|
118
|
+
}),
|
|
119
|
+
);
|
|
120
|
+
if (options.type) return nodes.filter((n) => n.type === options.type);
|
|
121
|
+
return nodes;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async readEdges() {
|
|
125
|
+
const issues = await this._issues();
|
|
126
|
+
const now = new Date().toISOString();
|
|
127
|
+
const edges = [];
|
|
128
|
+
const seen = new Set();
|
|
129
|
+
const push = (e) => {
|
|
130
|
+
if (seen.has(e.id)) return;
|
|
131
|
+
seen.add(e.id);
|
|
132
|
+
edges.push(e);
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
for (const iss of issues) {
|
|
136
|
+
const self = `issue:${iss.number}`;
|
|
137
|
+
const prov = (loc) => provenance({ provider: PROVIDER_ID, source: this.repo ? `${this.repo}#${iss.number}` : `#${iss.number}`, locator: loc, retrievedAt: now, agent: this.agent });
|
|
138
|
+
|
|
139
|
+
// metadata blockers -> blocks (blocker blocks this)
|
|
140
|
+
const md = parseWorkItemMetadata(iss.body);
|
|
141
|
+
for (const b of (md && Array.isArray(md.blockers) ? md.blockers : [])) {
|
|
142
|
+
const n = blockerNumber(b);
|
|
143
|
+
if (n == null) continue;
|
|
144
|
+
push(edge({ id: `issue:${n}--blocks--${iss.number}`, type: "blocks", from: `issue:${n}`, to: self, attributes: { origin: "metadata" }, provenance: prov("metadata.blockers") }));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const body = iss.body || "";
|
|
148
|
+
// prose "blocked by #N" / "depends on #N" -> blocks
|
|
149
|
+
for (const m of body.matchAll(/\b(?:blocked by|depends on|blocker:)\s*#(\d+)/gi)) {
|
|
150
|
+
const n = Number(m[1]);
|
|
151
|
+
push(edge({ id: `issue:${n}--blocks--${iss.number}`, type: "blocks", from: `issue:${n}`, to: self, attributes: { origin: "prose" }, provenance: prov("body.prose") }));
|
|
152
|
+
}
|
|
153
|
+
// other bare #N prose refs -> relates
|
|
154
|
+
for (const m of body.matchAll(/(?<![\w/])#(\d+)\b/g)) {
|
|
155
|
+
const n = Number(m[1]);
|
|
156
|
+
if (n === iss.number) continue;
|
|
157
|
+
const bid = `issue:${iss.number}--relates--${n}`;
|
|
158
|
+
if (seen.has(`issue:${n}--blocks--${iss.number}`)) continue; // already a stronger blocks edge
|
|
159
|
+
push(edge({ id: bid, type: "relates", from: self, to: `issue:${n}`, attributes: { origin: "prose" }, provenance: prov("body.prose") }));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return edges;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async queryByType(type) {
|
|
166
|
+
return this.readNodes({ type });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async readGraph() {
|
|
170
|
+
const [nodes, edges] = await Promise.all([this.readNodes(), this.readEdges()]);
|
|
171
|
+
return { nodes, edges };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Propose an issue comment or label change. Renders the draft an operator
|
|
176
|
+
* would file with gh — the provider never runs a mutating gh command.
|
|
177
|
+
*/
|
|
178
|
+
async proposeWrite(intent = {}) {
|
|
179
|
+
const { issue, kind = "comment", commentBody, labels = [] } = intent;
|
|
180
|
+
if (issue == null) throw new Error("work-item proposeWrite requires intent.issue (number)");
|
|
181
|
+
if (kind === "label") {
|
|
182
|
+
return proposal({
|
|
183
|
+
provider: PROVIDER_ID,
|
|
184
|
+
kind: "label",
|
|
185
|
+
target: { repo: this.repo, issue },
|
|
186
|
+
payload: { labels },
|
|
187
|
+
rendered: `gh issue edit ${issue}${this.repo ? ` --repo ${this.repo}` : ""} ${labels.map((l) => `--add-label ${JSON.stringify(l)}`).join(" ")}`,
|
|
188
|
+
rationale: intent.rationale || "Proposed label change (draft — operator files it).",
|
|
189
|
+
provenance: provenance({ provider: PROVIDER_ID, source: this.repo ? `${this.repo}#${issue}` : `#${issue}`, agent: this.agent }),
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
return proposal({
|
|
193
|
+
provider: PROVIDER_ID,
|
|
194
|
+
kind: "comment",
|
|
195
|
+
target: { repo: this.repo, issue },
|
|
196
|
+
payload: { body: commentBody || "" },
|
|
197
|
+
rendered: `gh issue comment ${issue}${this.repo ? ` --repo ${this.repo}` : ""} --body ${JSON.stringify(commentBody || "")}`,
|
|
198
|
+
rationale: intent.rationale || "Proposed issue comment (draft — operator files it).",
|
|
199
|
+
provenance: provenance({ provider: PROVIDER_ID, source: this.repo ? `${this.repo}#${issue}` : `#${issue}`, agent: this.agent }),
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export default WorkItemProvider;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kontourai/flow-agents",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "Flow Agents — a Kontour product that applies Flow and Veritas discipline as a portable process layer inside the agent tools you already use: Claude Code, Codex, Kiro, opencode, pi, and GitHub Actions — with framework adapters (AWS Strands preview) on the same policy-engine contract.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents",
|
|
@@ -110,6 +110,8 @@
|
|
|
110
110
|
"workflow:sidecar": "npm run build --silent && node build/src/cli/workflow-sidecar.js",
|
|
111
111
|
"workflow:validate-artifacts": "npm run build --silent && node build/src/cli/validate-workflow-artifacts.js",
|
|
112
112
|
"effective-backlog-settings": "npm run build --silent && node build/src/cli.js effective-backlog-settings",
|
|
113
|
+
"assignment-provider": "npm run build --silent && node build/src/cli.js assignment-provider",
|
|
114
|
+
"effective-assignment-provider-settings": "npm run build --silent && node build/src/cli.js effective-assignment-provider-settings",
|
|
113
115
|
"pull-work-provider": "npm run build --silent && node build/src/cli.js pull-work-provider",
|
|
114
116
|
"publish-change": "npm run build --silent && node build/src/cli.js publish-change",
|
|
115
117
|
"promote-workflow-artifact": "npm run build --silent && node build/src/cli.js promote-workflow-artifact",
|
|
@@ -141,7 +143,7 @@
|
|
|
141
143
|
"kit": "npm run build --silent && node build/src/cli.js kit"
|
|
142
144
|
},
|
|
143
145
|
"devDependencies": {
|
|
144
|
-
"@types/node": "^26.0
|
|
146
|
+
"@types/node": "^26.1.0",
|
|
145
147
|
"promptfoo": "^0.121.17",
|
|
146
148
|
"typescript": "^6.0.3"
|
|
147
149
|
},
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://flow-agents.dev/schemas/assignment-provider-settings.schema.json",
|
|
4
|
+
"title": "Flow Agents Assignment Provider Settings",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": ["schema_version"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"$schema": { "type": "string" },
|
|
10
|
+
"schema_version": { "const": "1.0" },
|
|
11
|
+
"defaults": { "$ref": "#/$defs/assignment_settings" },
|
|
12
|
+
"projects": {
|
|
13
|
+
"type": "array",
|
|
14
|
+
"items": { "$ref": "#/$defs/project_assignment_settings" }
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"anyOf": [{ "required": ["defaults"] }, { "required": ["projects"] }],
|
|
18
|
+
"$defs": {
|
|
19
|
+
"project_assignment_settings": {
|
|
20
|
+
"allOf": [
|
|
21
|
+
{ "$ref": "#/$defs/assignment_settings" },
|
|
22
|
+
{ "type": "object", "required": ["project"] }
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
"assignment_settings": {
|
|
26
|
+
"type": "object",
|
|
27
|
+
"additionalProperties": false,
|
|
28
|
+
"required": ["provider", "policy"],
|
|
29
|
+
"properties": {
|
|
30
|
+
"project": { "$ref": "#/$defs/project_match" },
|
|
31
|
+
"provider": { "$ref": "#/$defs/provider" },
|
|
32
|
+
"policy": { "$ref": "#/$defs/policy" }
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"project_match": {
|
|
36
|
+
"type": "object",
|
|
37
|
+
"additionalProperties": false,
|
|
38
|
+
"required": ["repo"],
|
|
39
|
+
"properties": {
|
|
40
|
+
"repo": { "$ref": "#/$defs/repo" },
|
|
41
|
+
"paths": { "type": "array", "items": { "type": "string", "minLength": 1 } }
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"provider": {
|
|
45
|
+
"oneOf": [
|
|
46
|
+
{ "$ref": "#/$defs/github_provider" },
|
|
47
|
+
{ "$ref": "#/$defs/local_file_provider" }
|
|
48
|
+
]
|
|
49
|
+
},
|
|
50
|
+
"github_provider": {
|
|
51
|
+
"type": "object",
|
|
52
|
+
"additionalProperties": false,
|
|
53
|
+
"required": ["kind", "repo", "capabilities"],
|
|
54
|
+
"properties": {
|
|
55
|
+
"kind": { "const": "github" },
|
|
56
|
+
"repo": { "$ref": "#/$defs/repo" },
|
|
57
|
+
"capabilities": { "$ref": "#/$defs/capabilities" }
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"local_file_provider": {
|
|
61
|
+
"type": "object",
|
|
62
|
+
"additionalProperties": false,
|
|
63
|
+
"required": ["kind", "capabilities"],
|
|
64
|
+
"properties": {
|
|
65
|
+
"kind": { "const": "local-file" },
|
|
66
|
+
"capabilities": { "$ref": "#/$defs/capabilities" }
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
"policy": {
|
|
70
|
+
"type": "object",
|
|
71
|
+
"additionalProperties": false,
|
|
72
|
+
"required": ["label_name", "claim_comment_marker", "human_assignee_policy"],
|
|
73
|
+
"properties": {
|
|
74
|
+
"label_name": {
|
|
75
|
+
"type": "string",
|
|
76
|
+
"minLength": 1,
|
|
77
|
+
"description": "Label attached to a claimed subject on providers that support labels. Default: \"agent:claimed\"."
|
|
78
|
+
},
|
|
79
|
+
"claim_comment_marker": {
|
|
80
|
+
"type": "string",
|
|
81
|
+
"minLength": 1,
|
|
82
|
+
"description": "Fixed marker used to locate the machine-readable claim comment among human comments. Default: \"<!-- flow-agents:assignment-claim -->\"."
|
|
83
|
+
},
|
|
84
|
+
"human_assignee_policy": { "$ref": "#/$defs/human_assignee_policy" },
|
|
85
|
+
"comment_refresh_on_phase_transition": {
|
|
86
|
+
"type": "boolean",
|
|
87
|
+
"description": "Whether an explicit render-claim/render-supersede call at a phase transition also refreshes the claim comment body. Default: false. No new mechanism; this reuses the same render path already built for claim."
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
"human_assignee_policy": {
|
|
92
|
+
"type": "object",
|
|
93
|
+
"additionalProperties": false,
|
|
94
|
+
"required": ["behavior", "idle_threshold_days"],
|
|
95
|
+
"properties": {
|
|
96
|
+
"behavior": {
|
|
97
|
+
"enum": ["ask_first", "never_reclaim"],
|
|
98
|
+
"description": "Default: \"ask_first\". A human assignee is never auto-superseded regardless of this value; this knob only governs whether idle human assignments are ever surfaced for possible reclaim confirmation (ask_first) or left untouched (never_reclaim)."
|
|
99
|
+
},
|
|
100
|
+
"idle_threshold_days": {
|
|
101
|
+
"type": "integer",
|
|
102
|
+
"minimum": 0,
|
|
103
|
+
"description": "Days of no linked activity before an idle human assignment is surfaced for confirmation. Default: 3."
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
"capabilities": {
|
|
108
|
+
"type": "array",
|
|
109
|
+
"items": {
|
|
110
|
+
"enum": ["assignees", "labels", "comments"]
|
|
111
|
+
},
|
|
112
|
+
"uniqueItems": true
|
|
113
|
+
},
|
|
114
|
+
"repo": {
|
|
115
|
+
"type": "object",
|
|
116
|
+
"additionalProperties": false,
|
|
117
|
+
"required": ["owner", "name"],
|
|
118
|
+
"properties": {
|
|
119
|
+
"owner": { "type": "string", "minLength": 1 },
|
|
120
|
+
"name": { "type": "string", "minLength": 1 },
|
|
121
|
+
"url": { "type": "string" }
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|