@thebackstoryis/engineering-with-ai 0.2.9 → 0.3.0-beta.2

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.
Files changed (37) hide show
  1. package/Docs/README.md +2 -0
  2. package/Docs/autonomous-intent-delivery.md +37 -0
  3. package/Docs/guide-catalogue.md +2 -0
  4. package/Docs/quality/persona-driven-test-scenarios.md +6 -0
  5. package/README.md +17 -0
  6. package/config/autonomy-worker.schema.json +70 -0
  7. package/config/autonomy.schema.json +25 -0
  8. package/package.json +3 -3
  9. package/public/app.js +7 -0
  10. package/public/autonomy.js +470 -0
  11. package/public/index.html +26 -0
  12. package/public/styles.css +64 -0
  13. package/skills-src/ewai-autonomy/SKILL.md +18 -0
  14. package/skills-src/ewai-autonomy/agents/openai.yaml +4 -0
  15. package/skills-src/ewai-deliver/SKILL.md +2 -0
  16. package/skills-src/ewai-test-scenarios/references/scenario-contract.md +6 -0
  17. package/src/autonomy-phase-contracts.mjs +88 -0
  18. package/src/autonomy-worker.mjs +15 -0
  19. package/src/autonomy.mjs +199 -0
  20. package/src/checkin.mjs +30 -6
  21. package/src/cli.mjs +67 -0
  22. package/src/delivery-gates.mjs +8 -1
  23. package/src/delivery.mjs +74 -3
  24. package/src/design-system-application.mjs +28 -0
  25. package/src/runtime/afk-conductor.mjs +191 -71
  26. package/src/runtime/autonomy-operations.mjs +250 -0
  27. package/src/runtime/autonomy-supervisor.mjs +426 -0
  28. package/src/runtime/autonomy-workers.mjs +333 -0
  29. package/src/runtime/autonomy-workspace.mjs +310 -0
  30. package/src/runtime/dashboard-actions.mjs +69 -0
  31. package/src/runtime/dashboard-server.mjs +25 -0
  32. package/src/runtime/database.mjs +25 -6
  33. package/src/runtime/intent-ownership.mjs +272 -0
  34. package/src/runtime/mcp-server.mjs +38 -0
  35. package/src/runtime/provider-adapters.mjs +365 -7
  36. package/src/runtime/work.mjs +21 -5
  37. package/src/test-scenarios.mjs +173 -20
@@ -1,9 +1,12 @@
1
1
  import { createHash, randomUUID } from 'node:crypto';
2
2
  import {
3
3
  existsSync,
4
+ linkSync,
5
+ lstatSync,
4
6
  mkdirSync,
5
7
  readdirSync,
6
8
  readFileSync,
9
+ realpathSync,
7
10
  renameSync,
8
11
  rmSync,
9
12
  writeFileSync
@@ -17,6 +20,9 @@ import { selectContextualPersonas } from './runtime/persona-engagement.mjs';
17
20
  const BRIEF_SCHEMA = 'ewai.persona-test-brief/v1';
18
21
  const SCENARIO_SCHEMA = 'ewai.persona-test-scenarios/v1';
19
22
  const WORKSPACE_SCHEMA = 'ewai.persona-test-workspace/v1';
23
+ const SOURCE_DIGEST_VERSION = 'intent-content-v2';
24
+ const BASELINE_SCHEMA = 'ewai.scenario-source-baseline/v1';
25
+ const lifecycleMetadata = new Set(['status', 'delivery_status', 'current_phase', 'delivery_state_path', 'updated_at']);
20
26
  const scenarioIdPattern = /^PTS-[0-9]{3}$/;
21
27
  const sourceIdPattern = /^[a-z0-9][a-z0-9:._-]{2,159}$/i;
22
28
  const allowedTypes = new Set([
@@ -54,6 +60,22 @@ function canonicalJson(value) {
54
60
  return JSON.stringify(canonical(value));
55
61
  }
56
62
 
63
+ function assertFingerprintMetadata(value, ancestors = new Set()) {
64
+ if (value === null || typeof value === 'string' || typeof value === 'boolean') return;
65
+ if (typeof value === 'number') {
66
+ if (!Number.isFinite(value)) throw new Error('Intent metadata contains a non-finite number');
67
+ return;
68
+ }
69
+ if (typeof value !== 'object' || (!Array.isArray(value)
70
+ && Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null)) {
71
+ throw new Error('Intent metadata contains an unsupported value; use plain mappings, arrays and JSON scalar values');
72
+ }
73
+ if (ancestors.has(value)) throw new Error('Intent metadata contains a cyclic value');
74
+ ancestors.add(value);
75
+ for (const child of Object.values(value)) assertFingerprintMetadata(child, ancestors);
76
+ ancestors.delete(value);
77
+ }
78
+
57
79
  function relativePath(root, path) {
58
80
  return relative(resolve(root), resolve(path)).replaceAll('\\', '/');
59
81
  }
@@ -260,13 +282,33 @@ function briefDigest(sources, contextualEvidence) {
260
282
  }));
261
283
  }
262
284
 
263
- export function preparePersonaTestScenarioBrief(projectRoot, slug, options = {}) {
285
+ function prepareBrief(projectRoot, slug, options = {}, sourceDigestVersion = SOURCE_DIGEST_VERSION) {
264
286
  const root = resolve(projectRoot);
265
287
  const focus = cleanText(options.focus, 'Focus', MAX_FOCUS, false);
266
288
  const intent = findIntent(root, cleanText(slug, 'Intent slug', 100));
267
289
  const paths = deliveryPaths(root, slug);
290
+ const intentEvidence = intentSources(root, intent);
291
+ // A planning source alone cannot carry the complete intent fingerprint.
292
+ if (!intentEvidence.length) {
293
+ throw new Error('A recognised intent source is required: capture Problem, Desired outcome, Constraints, or a listed Journey or Acceptance criterion');
294
+ }
295
+ const rawIntentDigest = sha256(intent.content);
296
+ // Both policies describe the same captured intent bytes, even if a later read changes.
297
+ for (const source of intentEvidence) source.digest = rawIntentDigest;
298
+ if (sourceDigestVersion === SOURCE_DIGEST_VERSION) {
299
+ if (!intent.metadata || typeof intent.metadata !== 'object' || Array.isArray(intent.metadata)) {
300
+ throw new Error('Intent metadata must be a mapping');
301
+ }
302
+ const metadata = Object.fromEntries(Object.entries(intent.metadata).filter(([key]) => !lifecycleMetadata.has(key)));
303
+ // Reject values canonical() cannot represent faithfully before it can erase their type/content.
304
+ assertFingerprintMetadata(metadata);
305
+ // Cover the complete body and every other metadata value, never the bounded excerpts.
306
+ const content = canonicalJson({ metadata, body: intent.body });
307
+ const digest = sha256(content);
308
+ for (const source of intentEvidence) source.digest = digest;
309
+ }
268
310
  const authoritativeSources = [
269
- ...intentSources(root, intent),
311
+ ...intentEvidence,
270
312
  ...planSources(root, paths.deliveryRoot),
271
313
  ...standardSources(root, intent.specsRoot)
272
314
  ];
@@ -283,11 +325,15 @@ export function preparePersonaTestScenarioBrief(projectRoot, slug, options = {})
283
325
  limit: 4
284
326
  });
285
327
  const sourceDigest = briefDigest(authoritativeSources, contextualEvidence);
328
+ const legacySourceDigest = briefDigest(authoritativeSources.map((source) => source.kind.startsWith('intent-')
329
+ ? { ...source, digest: rawIntentDigest } : source), contextualEvidence);
286
330
  return {
287
331
  schema: BRIEF_SCHEMA,
288
332
  slug,
289
333
  focus,
290
334
  sourceDigest,
335
+ legacySourceDigest,
336
+ ...(sourceDigestVersion ? { sourceDigestVersion } : {}),
291
337
  authoritativeSources,
292
338
  contextualEvidence,
293
339
  activePersonas,
@@ -307,6 +353,11 @@ export function preparePersonaTestScenarioBrief(projectRoot, slug, options = {})
307
353
  };
308
354
  }
309
355
 
356
+ export function preparePersonaTestScenarioBrief(projectRoot, slug, options = {}) {
357
+ const { legacySourceDigest: _legacySourceDigest, ...brief } = prepareBrief(projectRoot, slug, options);
358
+ return brief;
359
+ }
360
+
310
361
  function textList(value, label, { required = true } = {}) {
311
362
  if (!Array.isArray(value) || (required && !value.length) || value.length > MAX_LIST_ITEMS) {
312
363
  throw new Error(`${label} must contain ${required ? 'one to ' : 'no more than '}${MAX_LIST_ITEMS} items`);
@@ -394,6 +445,7 @@ function scenarioPayload(slug, brief, input, reviewer) {
394
445
  schema: SCENARIO_SCHEMA,
395
446
  slug,
396
447
  preparedSourceDigest: brief.sourceDigest,
448
+ ...(brief.sourceDigestVersion ? { sourceDigestVersion: brief.sourceDigestVersion } : {}),
397
449
  reviewer,
398
450
  focus: brief.focus,
399
451
  sources: brief.authoritativeSources.map(({ id, kind, label, path, digest, authority }) => ({ id, kind, label, path, digest, authority })),
@@ -510,26 +562,126 @@ function writePair(projectRoot, slug, jsonContent, markdownContent, options = {}
510
562
  return { jsonPath, markdownPath };
511
563
  }
512
564
 
565
+ function evidenceFile(root, path, label, maximum = Infinity) {
566
+ let stat;
567
+ try { stat = lstatSync(path); } catch (error) {
568
+ if (error.code === 'ENOENT') return false;
569
+ throw error;
570
+ }
571
+ if (!stat.isFile() || stat.isSymbolicLink() || !isWithin(realpathSync(root), realpathSync(path)) || stat.size > maximum) {
572
+ throw new Error(`${label} must be a bounded regular file inside the project`);
573
+ }
574
+ return true;
575
+ }
576
+
577
+ function readRecordedPair(root, paths, slug) {
578
+ const jsonPath = resolve(paths.deliveryRoot, 'test-scenarios.json');
579
+ const markdownPath = resolve(paths.deliveryRoot, 'test-scenarios.md');
580
+ const hasJson = evidenceFile(root, jsonPath, 'Scenario JSON');
581
+ const hasMarkdown = evidenceFile(root, markdownPath, 'Scenario Markdown');
582
+ if (!hasJson && !hasMarkdown) return null;
583
+ if (!hasJson || !hasMarkdown) throw new Error('Existing persona test-scenario evidence is incomplete and will not be overwritten');
584
+ const record = readJson(jsonPath, 'Persona test-scenario evidence');
585
+ if (record.schema !== SCENARIO_SCHEMA || record.slug !== slug) throw new Error('Persona test-scenario JSON schema or slug is invalid');
586
+ if (record.sourceDigestVersion !== undefined && record.sourceDigestVersion !== SOURCE_DIGEST_VERSION) {
587
+ throw new Error('Unsupported persona test-scenario source digest version');
588
+ }
589
+ if (sha256(canonicalJson(recordDigestPayload(record))) !== record.contentDigest) throw new Error('Persona test-scenario content digest does not match');
590
+ if (sha256(readFileSync(markdownPath, 'utf8')) !== record.markdownDigest) throw new Error('Persona test-scenario Markdown digest does not match');
591
+ return record;
592
+ }
593
+
594
+ function readSourceBaseline(root, paths, record) {
595
+ const path = resolve(paths.deliveryRoot, 'test-scenarios.source-baseline.json');
596
+ if (!evidenceFile(root, path, 'Scenario source baseline', 8192)) return null;
597
+ const baseline = readJson(path, 'Scenario source baseline');
598
+ const { digest, ...payload } = baseline;
599
+ const keys = ['schema', 'slug', 'recordDigest', 'legacySourceDigest', 'sourceDigestVersion', 'sourceDigest', 'reviewedBy', 'recordedAt', 'digest'];
600
+ if (Object.keys(baseline).sort().join() !== keys.sort().join()
601
+ || baseline.schema !== BASELINE_SCHEMA || baseline.slug !== record.slug
602
+ || record.sourceDigestVersion !== undefined
603
+ || baseline.recordDigest !== record.contentDigest || baseline.legacySourceDigest !== record.preparedSourceDigest
604
+ || baseline.sourceDigestVersion !== SOURCE_DIGEST_VERSION || !/^[a-f0-9]{64}$/.test(baseline.sourceDigest)
605
+ || baseline.reviewedBy !== record.reviewer || typeof baseline.recordedAt !== 'string' || !Number.isFinite(Date.parse(baseline.recordedAt))
606
+ || digest !== sha256(canonicalJson(payload))) {
607
+ throw new Error('Scenario source baseline is invalid or bound to different accepted evidence');
608
+ }
609
+ return baseline;
610
+ }
611
+
612
+ function writeSourceBaseline(root, paths, record, brief) {
613
+ const path = resolve(paths.deliveryRoot, 'test-scenarios.source-baseline.json');
614
+ const existing = readSourceBaseline(root, paths, record);
615
+ if (existing) {
616
+ if (existing.sourceDigest !== brief.sourceDigest) throw new Error('Scenario source baseline is stale');
617
+ return existing;
618
+ }
619
+ if (!isWithin(realpathSync(root), realpathSync(paths.deliveryRoot))) throw new Error('Scenario source baseline is outside the project');
620
+ const payload = {
621
+ schema: BASELINE_SCHEMA, slug: record.slug, recordDigest: record.contentDigest,
622
+ legacySourceDigest: record.preparedSourceDigest, sourceDigestVersion: SOURCE_DIGEST_VERSION,
623
+ sourceDigest: brief.sourceDigest, reviewedBy: record.reviewer, recordedAt: new Date().toISOString()
624
+ };
625
+ const baseline = { ...payload, digest: sha256(canonicalJson(payload)) };
626
+ const stage = `${path}.${process.pid}-${randomUUID()}.tmp`;
627
+ try {
628
+ writeFileSync(stage, `${JSON.stringify(baseline, null, 2)}\n`, { encoding: 'utf8', flag: 'wx', mode: 0o600 });
629
+ // A hard-link publish is atomic and cannot overwrite a competing or historical receipt.
630
+ try { linkSync(stage, path); } catch (error) {
631
+ if (error.code !== 'EEXIST') throw error;
632
+ const concurrent = readSourceBaseline(root, paths, record);
633
+ if (!concurrent || concurrent.sourceDigest !== brief.sourceDigest) throw new Error('Conflicting scenario source baseline');
634
+ return concurrent;
635
+ }
636
+ } finally { rmSync(stage, { force: true }); }
637
+ return baseline;
638
+ }
639
+
513
640
  export function recordPersonaTestScenarios(projectRoot, slug, input = {}, options = {}) {
514
641
  const root = resolve(projectRoot);
642
+ findIntent(root, slug);
515
643
  cleanTransaction(root, slug);
516
644
  const reviewer = cleanText(options.reviewedBy ?? input.reviewedBy, 'Named reviewer', 160);
645
+ if (input.sourceDigestVersion !== undefined && input.sourceDigestVersion !== SOURCE_DIGEST_VERSION) {
646
+ throw new Error('Unsupported persona test-scenario source digest version');
647
+ }
517
648
  const catalogue = Array.isArray(options.personas) ? options.personas : Array.isArray(options.personaCatalogue) ? options.personaCatalogue : [];
518
- const brief = preparePersonaTestScenarioBrief(root, slug, { focus: input.focus ?? options.focus ?? '', personas: catalogue });
519
- // Path validation needs the project root while the brief itself remains a safe projection.
520
- brief.projectRoot = root;
521
- const payload = scenarioPayload(slug, brief, input, reviewer);
522
- delete brief.projectRoot;
523
- const contentDigest = sha256(canonicalJson(payload));
649
+ const prepareOptions = { focus: input.focus ?? options.focus ?? '', personas: catalogue };
650
+ const { legacySourceDigest, ...brief } = prepareBrief(root, slug, prepareOptions);
524
651
  const paths = deliveryPaths(root, slug);
525
652
  const jsonPath = resolve(paths.deliveryRoot, 'test-scenarios.json');
526
653
  const markdownPath = resolve(paths.deliveryRoot, 'test-scenarios.md');
527
- if (existsSync(jsonPath) || existsSync(markdownPath)) {
528
- if (!existsSync(jsonPath) || !existsSync(markdownPath)) throw new Error('Existing persona test-scenario evidence is incomplete and will not be overwritten');
529
- const existing = readJson(jsonPath, 'Existing persona test-scenario evidence');
530
- const existingDigest = sha256(canonicalJson(recordDigestPayload(existing)));
531
- if (existing.contentDigest === contentDigest && existingDigest === contentDigest && sha256(readFileSync(markdownPath, 'utf8')) === existing.markdownDigest) {
532
- return { schema: 'ewai.persona-test-recording/v1', status: 'recorded', digest: contentDigest, jsonPath: relativePath(root, jsonPath), markdownPath: relativePath(root, markdownPath), idempotent: true };
654
+ const existing = readRecordedPair(root, paths, slug);
655
+ if (!existing && evidenceFile(root, resolve(paths.deliveryRoot, 'test-scenarios.source-baseline.json'), 'Scenario source baseline', 8192)) {
656
+ throw new Error('Scenario source baseline has no accepted evidence pair');
657
+ }
658
+ const baseline = existing ? readSourceBaseline(root, paths, existing) : null;
659
+ let validationBrief = brief;
660
+ let validationInput = input;
661
+ if (existing && existing.sourceDigestVersion === undefined) {
662
+ const currentDigest = baseline ? brief.sourceDigest : legacySourceDigest;
663
+ if (currentDigest !== (baseline?.sourceDigest ?? existing.preparedSourceDigest)) {
664
+ throw new Error('Existing accepted scenario sources are stale; legacy evidence cannot be silently rebound');
665
+ }
666
+ if (![existing.preparedSourceDigest, brief.sourceDigest].includes(input.preparedSourceDigest)) {
667
+ throw new Error('Prepared source digest is stale or does not match this intent');
668
+ }
669
+ // Compare the reviewed payload exactly, retaining its historical source and reviewer metadata.
670
+ validationBrief = { ...brief, sourceDigest: existing.preparedSourceDigest, authoritativeSources: existing.sources, contextualEvidence: existing.contextualEvidence };
671
+ delete validationBrief.sourceDigestVersion;
672
+ validationInput = { ...input, preparedSourceDigest: existing.preparedSourceDigest };
673
+ }
674
+ const payload = scenarioPayload(slug, { ...validationBrief, projectRoot: root }, validationInput, reviewer);
675
+ const contentDigest = sha256(canonicalJson(payload));
676
+ if (existing) {
677
+ if (existing.contentDigest === contentDigest) {
678
+ const legacy = existing.sourceDigestVersion === undefined;
679
+ if (legacy) writeSourceBaseline(root, paths, existing, brief);
680
+ return {
681
+ schema: 'ewai.persona-test-recording/v1', status: 'recorded', digest: contentDigest,
682
+ jsonPath: relativePath(root, jsonPath), markdownPath: relativePath(root, markdownPath), idempotent: true,
683
+ ...(legacy ? { sourceBaselinePath: relativePath(root, resolve(paths.deliveryRoot, 'test-scenarios.source-baseline.json')) } : {})
684
+ };
533
685
  }
534
686
  throw new Error('Conflicting accepted persona test-scenario evidence already exists and will not be overwritten');
535
687
  }
@@ -601,21 +753,22 @@ export function readPersonaTestScenarioWorkspace(projectRoot, slug, options = {}
601
753
  }
602
754
  let record;
603
755
  try {
604
- record = readJson(jsonPath, 'Persona test-scenario evidence');
605
- if (record.schema !== SCENARIO_SCHEMA || record.slug !== safeSlug) throw new Error('Persona test-scenario JSON schema or slug is invalid');
606
- if (sha256(canonicalJson(recordDigestPayload(record))) !== record.contentDigest) throw new Error('Persona test-scenario content digest does not match');
607
- if (sha256(readFileSync(markdownPath, 'utf8')) !== record.markdownDigest) throw new Error('Persona test-scenario Markdown digest does not match');
756
+ record = readRecordedPair(root, paths, safeSlug);
608
757
  } catch (error) {
609
758
  return { schema: WORKSPACE_SCHEMA, slug: safeSlug, status: 'invalid', reason: error.message, activePersonas: [], sources: [], scenarios: [], gaps: [], evidenceRoutes: [], guidance: { readOnly: true } };
610
759
  }
611
760
  const catalogue = Array.isArray(options.personas) ? options.personas : Array.isArray(options.personaCatalogue) ? options.personaCatalogue : [];
612
761
  let current;
762
+ let expectedDigest = record.preparedSourceDigest;
613
763
  try {
614
- current = preparePersonaTestScenarioBrief(root, safeSlug, { focus: record.focus, personas: catalogue });
764
+ const baseline = readSourceBaseline(root, paths, record);
765
+ current = prepareBrief(root, safeSlug, { focus: record.focus, personas: catalogue },
766
+ record.sourceDigestVersion ?? (baseline ? SOURCE_DIGEST_VERSION : null));
767
+ expectedDigest = baseline?.sourceDigest ?? expectedDigest;
615
768
  } catch (error) {
616
769
  return safeWorkspaceRecord(record, 'invalid', `Current authoritative sources cannot be read: ${error.message}`);
617
770
  }
618
- if (current.sourceDigest !== record.preparedSourceDigest) {
771
+ if (current.sourceDigest !== expectedDigest) {
619
772
  return safeWorkspaceRecord(record, 'stale', 'Authoritative source evidence changed after this pack was reviewed. Prepare and review it again.');
620
773
  }
621
774
  return safeWorkspaceRecord(record, 'recorded');