@kontourai/flow-agents 3.1.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 +4 -0
- package/CHANGELOG.md +17 -0
- package/build/src/cli/assignment-provider.d.ts +45 -0
- package/build/src/cli/assignment-provider.js +97 -12
- package/build/src/cli/workflow-sidecar.d.ts +14 -4
- package/build/src/cli/workflow-sidecar.js +100 -10
- package/context/contracts/assignment-provider-contract.md +1 -1
- package/context/contracts/execution-contract.md +78 -0
- package/context/scripts/hooks/config-protection.js +11 -4
- package/context/scripts/hooks/stop-goal-fit.js +259 -4
- package/docs/adr/0022-fail-closed-delivery-reconciliation-with-governed-exemptions.md +111 -0
- package/evals/ci/run-baseline.sh +2 -0
- package/evals/integration/test_checkpoint_signing.sh +4 -3
- package/evals/integration/test_gate_lockdown.sh +36 -0
- package/evals/integration/test_model_routing_escalation.sh +145 -0
- package/evals/integration/test_publish_delivery.sh +14 -6
- package/evals/integration/test_stop_hook_release.sh +552 -0
- package/evals/integration/test_trust_reconcile_negatives.sh +170 -0
- package/evals/run.sh +6 -0
- package/evals/static/test_model_routing_hints.sh +107 -0
- package/kits/builder/skills/builder-shape/SKILL.md +10 -0
- package/kits/builder/skills/deliver/SKILL.md +52 -11
- package/kits/builder/skills/design-probe/SKILL.md +10 -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/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 +56 -15
- package/kits/knowledge/adapters/flow-runner/index.js +912 -16
- package/kits/knowledge/adapters/obsidian-store/index.js +29 -11
- package/kits/knowledge/adapters/shared/codec.js +124 -0
- package/kits/knowledge/docs/store-contract.md +405 -3
- 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 +36 -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/package.json +1 -1
- package/schemas/workflow-handoff.schema.json +6 -0
- package/scripts/ci/mint-attestation.js +33 -6
- package/scripts/ci/trust-reconcile.js +144 -26
- package/scripts/hooks/config-protection.js +11 -4
- package/scripts/hooks/stop-goal-fit.js +259 -4
- package/src/cli/assignment-provider.ts +110 -12
- package/src/cli/workflow-sidecar.ts +99 -10
|
@@ -26,6 +26,10 @@ import {
|
|
|
26
26
|
loadAliasIndex,
|
|
27
27
|
saveAliasIndex,
|
|
28
28
|
registerAliases,
|
|
29
|
+
// Record-carried freshness + derived staleness (issue #341, Addendum J).
|
|
30
|
+
freshnessPatch,
|
|
31
|
+
decodeFreshnessFields,
|
|
32
|
+
isRecordStale,
|
|
29
33
|
} from "../shared/codec.js";
|
|
30
34
|
|
|
31
35
|
// ---------------------------------------------------------------------------
|
|
@@ -152,7 +156,17 @@ function parseYamlLines(lines, start, baseIndent) {
|
|
|
152
156
|
}
|
|
153
157
|
|
|
154
158
|
function unquote(s) {
|
|
155
|
-
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("'")) {
|
|
156
170
|
return s.slice(1, -1);
|
|
157
171
|
}
|
|
158
172
|
return s;
|
|
@@ -210,9 +224,20 @@ function serializeYaml(obj, indent = 0) {
|
|
|
210
224
|
|
|
211
225
|
function yamlScalar(v) {
|
|
212
226
|
if (typeof v === "string") {
|
|
213
|
-
// Quote if it contains special chars
|
|
214
|
-
|
|
215
|
-
|
|
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}"`;
|
|
216
241
|
}
|
|
217
242
|
return v;
|
|
218
243
|
}
|
|
@@ -398,7 +423,8 @@ export class DefaultKnowledgeStore {
|
|
|
398
423
|
if (!fs.existsSync(p)) return null;
|
|
399
424
|
const text = fs.readFileSync(p, "utf8");
|
|
400
425
|
const { meta, body } = parseMarkdown(text);
|
|
401
|
-
|
|
426
|
+
// Coerce record-carried freshness fields to canonical types (#341).
|
|
427
|
+
return decodeFreshnessFields({ ...meta, body });
|
|
402
428
|
}
|
|
403
429
|
|
|
404
430
|
_writeRecord(record) {
|
|
@@ -444,6 +470,10 @@ export class DefaultKnowledgeStore {
|
|
|
444
470
|
registerAliases(aliasIndex, id, aliases);
|
|
445
471
|
}
|
|
446
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
|
+
|
|
447
477
|
// Merge explicit links + wikilinks from body
|
|
448
478
|
const explicitLinks = input.links || [];
|
|
449
479
|
const wikilinks = extractWikilinks(input.body || "");
|
|
@@ -459,6 +489,7 @@ export class DefaultKnowledgeStore {
|
|
|
459
489
|
status: "active",
|
|
460
490
|
created_at: now,
|
|
461
491
|
updated_at: now,
|
|
492
|
+
...fresh,
|
|
462
493
|
provenance: {
|
|
463
494
|
agent: input.provenance.agent,
|
|
464
495
|
...(input.provenance.session_id ? { session_id: input.provenance.session_id } : {}),
|
|
@@ -494,7 +525,7 @@ export class DefaultKnowledgeStore {
|
|
|
494
525
|
const record = this._readRecord(id);
|
|
495
526
|
if (!record) throw notFoundError(id);
|
|
496
527
|
|
|
497
|
-
const mutableKeys = ["title", "body", "category", "tags", "links", "aliases"];
|
|
528
|
+
const mutableKeys = ["title", "body", "category", "tags", "links", "aliases", "expires_at", "ttl_seconds"];
|
|
498
529
|
const supplied = mutableKeys.filter((k) => fields[k] !== undefined);
|
|
499
530
|
if (supplied.length === 0)
|
|
500
531
|
throw missingEvidenceError("update: at least one mutable field must be supplied");
|
|
@@ -502,6 +533,10 @@ export class DefaultKnowledgeStore {
|
|
|
502
533
|
if (fields.category !== undefined && !validateCategory(fields.category))
|
|
503
534
|
throw missingEvidenceError(`update: invalid category: ${fields.category}`);
|
|
504
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
|
+
|
|
505
540
|
const now = this._now();
|
|
506
541
|
|
|
507
542
|
// Slug aliases are append-only: supplied aliases are UNIONED with existing
|
|
@@ -533,6 +568,7 @@ export class DefaultKnowledgeStore {
|
|
|
533
568
|
...(fields.category !== undefined ? { category: fields.category } : {}),
|
|
534
569
|
...(fields.tags !== undefined ? { tags: fields.tags } : {}),
|
|
535
570
|
...(mergedAliases.length ? { aliases: mergedAliases } : {}),
|
|
571
|
+
...fresh,
|
|
536
572
|
links: newLinks,
|
|
537
573
|
updated_at: now,
|
|
538
574
|
mutation_log: [
|
|
@@ -943,18 +979,19 @@ export class DefaultKnowledgeStore {
|
|
|
943
979
|
async listByCategory(category, options = {}) {
|
|
944
980
|
const records = this._allRecords();
|
|
945
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));
|
|
946
989
|
if (options.prefix) {
|
|
947
990
|
return records.filter(
|
|
948
|
-
(r) =>
|
|
949
|
-
(r.category === category || r.category.startsWith(`${category}.`)) &&
|
|
950
|
-
(includeRetired || (r.status || "active") !== "retired")
|
|
991
|
+
(r) => (r.category === category || r.category.startsWith(`${category}.`)) && keep(r)
|
|
951
992
|
);
|
|
952
993
|
}
|
|
953
|
-
return records.filter(
|
|
954
|
-
(r) =>
|
|
955
|
-
r.category === category &&
|
|
956
|
-
(includeRetired || (r.status || "active") !== "retired")
|
|
957
|
-
);
|
|
994
|
+
return records.filter((r) => r.category === category && keep(r));
|
|
958
995
|
}
|
|
959
996
|
|
|
960
997
|
// -------------------------------------------------------------------------
|
|
@@ -963,10 +1000,14 @@ export class DefaultKnowledgeStore {
|
|
|
963
1000
|
|
|
964
1001
|
async listByType(type, options = {}) {
|
|
965
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();
|
|
966
1006
|
return this._allRecords().filter(
|
|
967
1007
|
(r) =>
|
|
968
1008
|
r.type === type &&
|
|
969
|
-
(includeRetired || (r.status || "active") !== "retired")
|
|
1009
|
+
(includeRetired || (r.status || "active") !== "retired") &&
|
|
1010
|
+
(!staleOnly || isRecordStale(r, nowMs))
|
|
970
1011
|
);
|
|
971
1012
|
}
|
|
972
1013
|
|