@principles/core 1.237.0 → 1.239.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.
Files changed (35) hide show
  1. package/dist/runtime-v2/__tests__/artificer-runner-vslice.test.js +839 -0
  2. package/dist/runtime-v2/__tests__/artificer-runner-vslice.test.js.map +1 -1
  3. package/dist/runtime-v2/__tests__/evaluator-repair-loop.test.d.ts +2 -0
  4. package/dist/runtime-v2/__tests__/evaluator-repair-loop.test.d.ts.map +1 -0
  5. package/dist/runtime-v2/__tests__/evaluator-repair-loop.test.js +562 -0
  6. package/dist/runtime-v2/__tests__/evaluator-repair-loop.test.js.map +1 -0
  7. package/dist/runtime-v2/__tests__/pitask-metadata.test.js +150 -0
  8. package/dist/runtime-v2/__tests__/pitask-metadata.test.js.map +1 -1
  9. package/dist/runtime-v2/feature-flags/__tests__/feature-flag-contract.test.js +32 -0
  10. package/dist/runtime-v2/feature-flags/__tests__/feature-flag-contract.test.js.map +1 -1
  11. package/dist/runtime-v2/feature-flags/feature-flag-contract.d.ts.map +1 -1
  12. package/dist/runtime-v2/feature-flags/feature-flag-contract.js +7 -0
  13. package/dist/runtime-v2/feature-flags/feature-flag-contract.js.map +1 -1
  14. package/dist/runtime-v2/internalization/__tests__/artificer-prompt-builder.test.js +133 -0
  15. package/dist/runtime-v2/internalization/__tests__/artificer-prompt-builder.test.js.map +1 -1
  16. package/dist/runtime-v2/internalization/artificer-prompt-builder.d.ts +42 -1
  17. package/dist/runtime-v2/internalization/artificer-prompt-builder.d.ts.map +1 -1
  18. package/dist/runtime-v2/internalization/artificer-prompt-builder.js +20 -0
  19. package/dist/runtime-v2/internalization/artificer-prompt-builder.js.map +1 -1
  20. package/dist/runtime-v2/internalization/artificer-runner.d.ts +22 -0
  21. package/dist/runtime-v2/internalization/artificer-runner.d.ts.map +1 -1
  22. package/dist/runtime-v2/internalization/artificer-runner.js +188 -2
  23. package/dist/runtime-v2/internalization/artificer-runner.js.map +1 -1
  24. package/dist/runtime-v2/internalization/evaluator-runner.d.ts +85 -0
  25. package/dist/runtime-v2/internalization/evaluator-runner.d.ts.map +1 -1
  26. package/dist/runtime-v2/internalization/evaluator-runner.js +223 -0
  27. package/dist/runtime-v2/internalization/evaluator-runner.js.map +1 -1
  28. package/dist/runtime-v2/internalization/peer-runner-contracts.d.ts +3 -0
  29. package/dist/runtime-v2/internalization/peer-runner-contracts.d.ts.map +1 -1
  30. package/dist/runtime-v2/internalization/peer-runner-contracts.js.map +1 -1
  31. package/dist/runtime-v2/internalization/pitask-metadata.d.ts +34 -0
  32. package/dist/runtime-v2/internalization/pitask-metadata.d.ts.map +1 -1
  33. package/dist/runtime-v2/internalization/pitask-metadata.js +64 -0
  34. package/dist/runtime-v2/internalization/pitask-metadata.js.map +1 -1
  35. package/package.json +1 -1
@@ -376,6 +376,7 @@ describe('ArtificerRunner.validateOutput — v2 mode-error errorCategory (CodeRa
376
376
  scribeArtifact: null,
377
377
  sourceScribeArtifactId: null,
378
378
  adversarialFeedback: null,
379
+ repairFeedback: null,
379
380
  };
380
381
  const result = await runner.validateOutput(output, ARTIFICER_TASK_ID, context);
381
382
  expect(result.valid).toBe(false);
@@ -662,4 +663,842 @@ describe('ArtificerRunner integration: test-double captures sourceScribeArtifact
662
663
  expect(storedOutput.sourceTrace.scribeArtifactId).toBe(SCRIBE_ART_ID);
663
664
  });
664
665
  });
666
+ describe('PRI-508: ArtificerRunner.buildContext reads dreamer artifact via scribe.sourceTrace.dreamerArtifactId', () => {
667
+ // Vertical slice 4: end-to-end — dreamer artifact in store → prompt contains dreamerContext 5维字段
668
+ const SCRIBE_ART_ID = 'pi-art-scribe-pri508';
669
+ const DREAMER_ART_ID = 'pi-art-dreamer-pri508';
670
+ const SCRIBE_TASK_ID_PRI508 = 'scribe-pri508';
671
+ const ARTIFICER_TASK_ID_PRI508 = 'artificer-pri508';
672
+ function makeScribeArtifactWithDreamer() {
673
+ return {
674
+ artifactId: SCRIBE_ART_ID,
675
+ artifactKind: 'principle',
676
+ sourceTaskId: SCRIBE_TASK_ID_PRI508,
677
+ lineageArtifactIds: [DREAMER_ART_ID],
678
+ validationStatus: 'pending',
679
+ contentJson: JSON.stringify({
680
+ taskId: SCRIBE_TASK_ID_PRI508,
681
+ sourcePhilosopherArtifactId: 'pi-art-philosopher-pri508',
682
+ principleDraft: {
683
+ title: 'Path Traversal Guard',
684
+ statement: 'Validate parent path before write_file',
685
+ rationale: 'Unchecked parent path enables path traversal',
686
+ applicability: ['write_file'],
687
+ antiPatterns: ['Trusting raw user input path'],
688
+ confidence: 0.9,
689
+ },
690
+ sourceTrace: {
691
+ philosopherArtifactId: 'pi-art-philosopher-pri508',
692
+ dreamerArtifactId: DREAMER_ART_ID,
693
+ },
694
+ risks: [],
695
+ generatedAt: new Date().toISOString(),
696
+ }),
697
+ createdAt: new Date().toISOString(),
698
+ updatedAt: new Date().toISOString(),
699
+ };
700
+ }
701
+ function makeDreamerArtifact() {
702
+ return {
703
+ artifactId: DREAMER_ART_ID,
704
+ artifactKind: 'principle',
705
+ sourceTaskId: 'dreamer-task-pri508',
706
+ lineageArtifactIds: [],
707
+ validationStatus: 'pending',
708
+ contentJson: JSON.stringify({
709
+ valid: true,
710
+ taskId: 'dreamer-task-pri508',
711
+ candidates: [
712
+ {
713
+ candidateIndex: 0,
714
+ badDecision: 'agent called write_file without resolving parent path',
715
+ betterDecision: 'agent must resolve and validate parent path before write_file',
716
+ rationale: 'parent path resolution prevents path traversal exploits',
717
+ confidence: 0.85,
718
+ riskLevel: 'medium',
719
+ strategicPerspective: 'proactive validation beats reactive cleanup',
720
+ },
721
+ ],
722
+ contextRefs: [],
723
+ generatedAt: new Date().toISOString(),
724
+ }),
725
+ createdAt: new Date().toISOString(),
726
+ updatedAt: new Date().toISOString(),
727
+ };
728
+ }
729
+ function makeScribeTaskPri508() {
730
+ return {
731
+ taskId: SCRIBE_TASK_ID_PRI508,
732
+ taskKind: 'scribe',
733
+ status: 'succeeded',
734
+ attemptCount: 1,
735
+ maxAttempts: 3,
736
+ resultRef: 'scribe://run-pri508',
737
+ createdAt: new Date().toISOString(),
738
+ updatedAt: new Date().toISOString(),
739
+ diagnosticJson: createPITaskDiagnosticJson({
740
+ dependencyTaskIds: [],
741
+ channel: 'prompt',
742
+ timeoutMs: 300_000,
743
+ inputArtifactRefs: [],
744
+ outputArtifactRefs: [{ artifactType: 'principle', ref: SCRIBE_ART_ID }],
745
+ }),
746
+ };
747
+ }
748
+ function makeArtificerTaskPri508() {
749
+ return {
750
+ taskId: ARTIFICER_TASK_ID_PRI508,
751
+ taskKind: 'artificer',
752
+ status: 'pending',
753
+ attemptCount: 0,
754
+ maxAttempts: 3,
755
+ createdAt: new Date().toISOString(),
756
+ updatedAt: new Date().toISOString(),
757
+ diagnosticJson: createPITaskDiagnosticJson({
758
+ dependencyTaskIds: [SCRIBE_TASK_ID_PRI508],
759
+ channel: 'prompt',
760
+ timeoutMs: 300_000,
761
+ inputArtifactRefs: [{ artifactType: 'principle', ref: SCRIBE_ART_ID }],
762
+ outputArtifactRefs: [],
763
+ }),
764
+ };
765
+ }
766
+ function makeArtificerOutputPri508() {
767
+ return {
768
+ taskId: ARTIFICER_TASK_ID_PRI508,
769
+ sourceScribeArtifactId: SCRIBE_ART_ID,
770
+ implementationCode: 'function evaluate(input, helpers) { return { decision: "allow", matched: false, reason: "ok" }; }',
771
+ goldenTraceCases: [
772
+ { caseId: 'positive-1', kind: 'positive', toolName: 'write_file', params: { path: '/workspace/file' }, expectedDecision: 'allow' },
773
+ { caseId: 'negative-1', kind: 'negative', toolName: 'write_file', params: { path: '/etc/passwd' }, expectedDecision: 'block' },
774
+ ],
775
+ affectedTools: ['write_file'],
776
+ implementationSummary: 'Validate parent path before write',
777
+ sourceTrace: {
778
+ scribeArtifactId: SCRIBE_ART_ID,
779
+ },
780
+ risks: [],
781
+ generatedAt: new Date().toISOString(),
782
+ };
783
+ }
784
+ it('dreamerContext 5维字段透传到 artificer prompt (end-to-end)', async () => {
785
+ const artifactStore = new MemoryPIArtifactStore();
786
+ await artifactStore.upsertArtifact(makeScribeArtifactWithDreamer());
787
+ await artifactStore.upsertArtifact(makeDreamerArtifact());
788
+ const artificerTask = makeArtificerTaskPri508();
789
+ const scribeTask = makeScribeTaskPri508();
790
+ const stateManager = {
791
+ acquireLease: vi.fn().mockResolvedValue(artificerTask),
792
+ getTask: vi.fn().mockImplementation((id) => {
793
+ if (id === ARTIFICER_TASK_ID_PRI508)
794
+ return Promise.resolve(artificerTask);
795
+ if (id === SCRIBE_TASK_ID_PRI508)
796
+ return Promise.resolve(scribeTask);
797
+ return Promise.resolve(null);
798
+ }),
799
+ getRunsByTask: vi.fn().mockResolvedValue([{
800
+ runId: 'run-pri508',
801
+ taskId: ARTIFICER_TASK_ID_PRI508,
802
+ runtimeKind: 'artificer',
803
+ startedAt: new Date().toISOString(),
804
+ }]),
805
+ getValidRunsByTaskTolerant: vi.fn().mockResolvedValue({
806
+ runs: [{ runId: 'run-pri508', taskId: ARTIFICER_TASK_ID_PRI508, runtimeKind: 'artificer', startedAt: new Date().toISOString() }],
807
+ degradedRuns: [],
808
+ }),
809
+ updateRunOutput: vi.fn().mockResolvedValue(undefined),
810
+ markTaskSucceeded: vi.fn().mockResolvedValue(undefined),
811
+ markTaskFailed: vi.fn().mockResolvedValue(undefined),
812
+ markTaskRetryWait: vi.fn().mockResolvedValue(undefined),
813
+ getRetryPolicy: vi.fn().mockReturnValue({ shouldRetry: () => false }),
814
+ };
815
+ let capturedPrompt;
816
+ const runtimeAdapter = {
817
+ startRun: vi.fn().mockImplementation((input) => {
818
+ capturedPrompt = typeof input.inputPayload === 'string' ? input.inputPayload : JSON.stringify(input.inputPayload);
819
+ return Promise.resolve({ runId: 'run-pri508', runtimeKind: 'test-double', startedAt: new Date().toISOString() });
820
+ }),
821
+ pollRun: vi.fn().mockResolvedValue({ status: 'succeeded', runId: 'run-pri508' }),
822
+ fetchOutput: vi.fn().mockResolvedValue({ payload: makeArtificerOutputPri508() }),
823
+ cancelRun: vi.fn().mockResolvedValue(undefined),
824
+ };
825
+ const eventEmitter = { emitTelemetry: vi.fn() };
826
+ const validator = new DefaultArtificerValidator();
827
+ const deps = {
828
+ stateManager,
829
+ runtimeAdapter,
830
+ eventEmitter,
831
+ artifactStore,
832
+ validator,
833
+ };
834
+ const runner = new ArtificerRunner(deps, {
835
+ owner: 'test',
836
+ runtimeKind: 'artificer',
837
+ pollIntervalMs: 10,
838
+ timeoutMs: 1000,
839
+ });
840
+ const result = await runner.run(ARTIFICER_TASK_ID_PRI508);
841
+ expect(result.status).toBe('succeeded');
842
+ // Assert the prompt captured at startRun contains dreamerContext 5维字段
843
+ expect(capturedPrompt).toBeDefined();
844
+ const parsed = JSON.parse(capturedPrompt);
845
+ expect(parsed.dreamerContext).toBeDefined();
846
+ expect(parsed.dreamerContext.badDecision).toBe('agent called write_file without resolving parent path');
847
+ expect(parsed.dreamerContext.betterDecision).toBe('agent must resolve and validate parent path before write_file');
848
+ expect(parsed.dreamerContext.rationale).toBe('parent path resolution prevents path traversal exploits');
849
+ expect(parsed.dreamerContext.riskLevel).toBe('medium');
850
+ expect(parsed.dreamerContext.strategicPerspective).toBe('proactive validation beats reactive cleanup');
851
+ });
852
+ it('dreamerArtifactId 缺失时 dreamerContext undefined (向后兼容)', async () => {
853
+ // scribe artifact 不带 sourceTrace.dreamerArtifactId → dreamerContext 不出现在 prompt
854
+ const artifactStore = new MemoryPIArtifactStore();
855
+ const scribeArtifactNoDreamer = {
856
+ artifactId: SCRIBE_ART_ID,
857
+ artifactKind: 'principle',
858
+ sourceTaskId: SCRIBE_TASK_ID_PRI508,
859
+ lineageArtifactIds: [],
860
+ validationStatus: 'pending',
861
+ contentJson: JSON.stringify({
862
+ taskId: SCRIBE_TASK_ID_PRI508,
863
+ sourcePhilosopherArtifactId: 'pi-art-philosopher-pri508',
864
+ principleDraft: {
865
+ title: 'Path Traversal Guard',
866
+ statement: 'Validate parent path before write_file',
867
+ rationale: 'Unchecked parent path enables path traversal',
868
+ applicability: ['write_file'],
869
+ antiPatterns: ['Trusting raw user input path'],
870
+ confidence: 0.9,
871
+ },
872
+ sourceTrace: {
873
+ philosopherArtifactId: 'pi-art-philosopher-pri508',
874
+ // dreamerArtifactId 刻意缺失
875
+ },
876
+ risks: [],
877
+ generatedAt: new Date().toISOString(),
878
+ }),
879
+ createdAt: new Date().toISOString(),
880
+ updatedAt: new Date().toISOString(),
881
+ };
882
+ await artifactStore.upsertArtifact(scribeArtifactNoDreamer);
883
+ const artificerTask = makeArtificerTaskPri508();
884
+ const scribeTask = makeScribeTaskPri508();
885
+ const stateManager = {
886
+ acquireLease: vi.fn().mockResolvedValue(artificerTask),
887
+ getTask: vi.fn().mockImplementation((id) => {
888
+ if (id === ARTIFICER_TASK_ID_PRI508)
889
+ return Promise.resolve(artificerTask);
890
+ if (id === SCRIBE_TASK_ID_PRI508)
891
+ return Promise.resolve(scribeTask);
892
+ return Promise.resolve(null);
893
+ }),
894
+ getRunsByTask: vi.fn().mockResolvedValue([{ runId: 'run-pri508-nodreamer', taskId: ARTIFICER_TASK_ID_PRI508, runtimeKind: 'artificer', startedAt: new Date().toISOString() }]),
895
+ getValidRunsByTaskTolerant: vi.fn().mockResolvedValue({ runs: [{ runId: 'run-pri508-nodreamer', taskId: ARTIFICER_TASK_ID_PRI508, runtimeKind: 'artificer', startedAt: new Date().toISOString() }], degradedRuns: [] }),
896
+ updateRunOutput: vi.fn().mockResolvedValue(undefined),
897
+ markTaskSucceeded: vi.fn().mockResolvedValue(undefined),
898
+ markTaskFailed: vi.fn().mockResolvedValue(undefined),
899
+ markTaskRetryWait: vi.fn().mockResolvedValue(undefined),
900
+ getRetryPolicy: vi.fn().mockReturnValue({ shouldRetry: () => false }),
901
+ };
902
+ let capturedPrompt;
903
+ const runtimeAdapter = {
904
+ startRun: vi.fn().mockImplementation((input) => {
905
+ capturedPrompt = typeof input.inputPayload === 'string' ? input.inputPayload : JSON.stringify(input.inputPayload);
906
+ return Promise.resolve({ runId: 'run-pri508-nodreamer', runtimeKind: 'test-double', startedAt: new Date().toISOString() });
907
+ }),
908
+ pollRun: vi.fn().mockResolvedValue({ status: 'succeeded', runId: 'run-pri508-nodreamer' }),
909
+ fetchOutput: vi.fn().mockResolvedValue({ payload: makeArtificerOutputPri508() }),
910
+ cancelRun: vi.fn().mockResolvedValue(undefined),
911
+ };
912
+ const eventEmitter = { emitTelemetry: vi.fn() };
913
+ const validator = new DefaultArtificerValidator();
914
+ const deps = {
915
+ stateManager,
916
+ runtimeAdapter,
917
+ eventEmitter,
918
+ artifactStore,
919
+ validator,
920
+ };
921
+ const runner = new ArtificerRunner(deps, {
922
+ owner: 'test',
923
+ runtimeKind: 'artificer',
924
+ pollIntervalMs: 10,
925
+ timeoutMs: 1000,
926
+ });
927
+ const result = await runner.run(ARTIFICER_TASK_ID_PRI508);
928
+ expect(result.status).toBe('succeeded');
929
+ expect(capturedPrompt).toBeDefined();
930
+ const parsed = JSON.parse(capturedPrompt);
931
+ expect(parsed.dreamerContext).toBeUndefined();
932
+ });
933
+ it('dreamerArtifactId 存在但 artifact 查不到时 emit event + dreamerContext undefined (rc-9)', async () => {
934
+ // scribe 带 dreamerArtifactId 但 store 里没有 dreamer artifact → 不能静默成功,要有可观察事件
935
+ const artifactStore = new MemoryPIArtifactStore();
936
+ await artifactStore.upsertArtifact(makeScribeArtifactWithDreamer());
937
+ // 故意不 upsert dreamer artifact
938
+ const artificerTask = makeArtificerTaskPri508();
939
+ const scribeTask = makeScribeTaskPri508();
940
+ const stateManager = {
941
+ acquireLease: vi.fn().mockResolvedValue(artificerTask),
942
+ getTask: vi.fn().mockImplementation((id) => {
943
+ if (id === ARTIFICER_TASK_ID_PRI508)
944
+ return Promise.resolve(artificerTask);
945
+ if (id === SCRIBE_TASK_ID_PRI508)
946
+ return Promise.resolve(scribeTask);
947
+ return Promise.resolve(null);
948
+ }),
949
+ getRunsByTask: vi.fn().mockResolvedValue([{ runId: 'run-pri508-missing', taskId: ARTIFICER_TASK_ID_PRI508, runtimeKind: 'artificer', startedAt: new Date().toISOString() }]),
950
+ getValidRunsByTaskTolerant: vi.fn().mockResolvedValue({ runs: [{ runId: 'run-pri508-missing', taskId: ARTIFICER_TASK_ID_PRI508, runtimeKind: 'artificer', startedAt: new Date().toISOString() }], degradedRuns: [] }),
951
+ updateRunOutput: vi.fn().mockResolvedValue(undefined),
952
+ markTaskSucceeded: vi.fn().mockResolvedValue(undefined),
953
+ markTaskFailed: vi.fn().mockResolvedValue(undefined),
954
+ markTaskRetryWait: vi.fn().mockResolvedValue(undefined),
955
+ getRetryPolicy: vi.fn().mockReturnValue({ shouldRetry: () => false }),
956
+ };
957
+ let capturedPrompt;
958
+ const runtimeAdapter = {
959
+ startRun: vi.fn().mockImplementation((input) => {
960
+ capturedPrompt = typeof input.inputPayload === 'string' ? input.inputPayload : JSON.stringify(input.inputPayload);
961
+ return Promise.resolve({ runId: 'run-pri508-missing', runtimeKind: 'test-double', startedAt: new Date().toISOString() });
962
+ }),
963
+ pollRun: vi.fn().mockResolvedValue({ status: 'succeeded', runId: 'run-pri508-missing' }),
964
+ fetchOutput: vi.fn().mockResolvedValue({ payload: makeArtificerOutputPri508() }),
965
+ cancelRun: vi.fn().mockResolvedValue(undefined),
966
+ };
967
+ const emitTelemetry = vi.fn();
968
+ const eventEmitter = { emitTelemetry };
969
+ const validator = new DefaultArtificerValidator();
970
+ const deps = {
971
+ stateManager,
972
+ runtimeAdapter,
973
+ eventEmitter,
974
+ artifactStore,
975
+ validator,
976
+ };
977
+ const runner = new ArtificerRunner(deps, {
978
+ owner: 'test',
979
+ runtimeKind: 'artificer',
980
+ pollIntervalMs: 10,
981
+ timeoutMs: 1000,
982
+ });
983
+ const result = await runner.run(ARTIFICER_TASK_ID_PRI508);
984
+ // 仍应成功(dreamerContext 是 best-effort,不阻塞主流程)
985
+ expect(result.status).toBe('succeeded');
986
+ // rc-9: 不能静默 — 必须有可观察事件
987
+ // emitTelemetry 接收单个结构化对象 (eventType + traceId + payload + ...)
988
+ expect(emitTelemetry).toHaveBeenCalledWith(expect.objectContaining({
989
+ eventType: expect.stringContaining('dreamer'),
990
+ traceId: ARTIFICER_TASK_ID_PRI508,
991
+ }));
992
+ expect(capturedPrompt).toBeDefined();
993
+ const parsed = JSON.parse(capturedPrompt);
994
+ expect(parsed.dreamerContext).toBeUndefined();
995
+ });
996
+ // ── PRI-508 edge cases: cover resolveDreamerContext() failure branches (rc-9) ──
997
+ // Each case asserts: (a) runner still succeeds (best-effort, non-blocking),
998
+ // (b) dreamerContext undefined in prompt, (c) emitTelemetry called with the
999
+ // expected observable event name so the gap is not silent.
1000
+ /**
1001
+ * Shared harness for PRI-508 edge-case tests. Accepts a custom scribe artifact
1002
+ * and optional dreamer artifact, runs the artificer end-to-end, and returns
1003
+ * the captured prompt + telemetry spy so each case can assert behavior.
1004
+ */
1005
+ async function runPri508EdgeCase(params) {
1006
+ const artifactStore = new MemoryPIArtifactStore();
1007
+ await artifactStore.upsertArtifact(params.scribeArtifact);
1008
+ if (params.dreamerArtifact) {
1009
+ await artifactStore.upsertArtifact(params.dreamerArtifact);
1010
+ }
1011
+ const artificerTask = makeArtificerTaskPri508();
1012
+ const scribeTask = makeScribeTaskPri508();
1013
+ const stateManager = {
1014
+ acquireLease: vi.fn().mockResolvedValue(artificerTask),
1015
+ getTask: vi.fn().mockImplementation((id) => {
1016
+ if (id === ARTIFICER_TASK_ID_PRI508)
1017
+ return Promise.resolve(artificerTask);
1018
+ if (id === SCRIBE_TASK_ID_PRI508)
1019
+ return Promise.resolve(scribeTask);
1020
+ return Promise.resolve(null);
1021
+ }),
1022
+ getRunsByTask: vi.fn().mockResolvedValue([{ runId: 'run-pri508-edge', taskId: ARTIFICER_TASK_ID_PRI508, runtimeKind: 'artificer', startedAt: new Date().toISOString() }]),
1023
+ getValidRunsByTaskTolerant: vi.fn().mockResolvedValue({ runs: [{ runId: 'run-pri508-edge', taskId: ARTIFICER_TASK_ID_PRI508, runtimeKind: 'artificer', startedAt: new Date().toISOString() }], degradedRuns: [] }),
1024
+ updateRunOutput: vi.fn().mockResolvedValue(undefined),
1025
+ markTaskSucceeded: vi.fn().mockResolvedValue(undefined),
1026
+ markTaskFailed: vi.fn().mockResolvedValue(undefined),
1027
+ markTaskRetryWait: vi.fn().mockResolvedValue(undefined),
1028
+ getRetryPolicy: vi.fn().mockReturnValue({ shouldRetry: () => false }),
1029
+ };
1030
+ let capturedPrompt;
1031
+ const runtimeAdapter = {
1032
+ startRun: vi.fn().mockImplementation((input) => {
1033
+ capturedPrompt = typeof input.inputPayload === 'string' ? input.inputPayload : JSON.stringify(input.inputPayload);
1034
+ return Promise.resolve({ runId: 'run-pri508-edge', runtimeKind: 'test-double', startedAt: new Date().toISOString() });
1035
+ }),
1036
+ pollRun: vi.fn().mockResolvedValue({ status: 'succeeded', runId: 'run-pri508-edge' }),
1037
+ fetchOutput: vi.fn().mockResolvedValue({ payload: makeArtificerOutputPri508() }),
1038
+ cancelRun: vi.fn().mockResolvedValue(undefined),
1039
+ };
1040
+ const emitTelemetry = vi.fn();
1041
+ const eventEmitter = { emitTelemetry };
1042
+ const validator = new DefaultArtificerValidator();
1043
+ const deps = {
1044
+ stateManager,
1045
+ runtimeAdapter,
1046
+ eventEmitter,
1047
+ artifactStore,
1048
+ validator,
1049
+ };
1050
+ const runner = new ArtificerRunner(deps, {
1051
+ owner: 'test',
1052
+ runtimeKind: 'artificer',
1053
+ pollIntervalMs: 10,
1054
+ timeoutMs: 1000,
1055
+ });
1056
+ const result = await runner.run(ARTIFICER_TASK_ID_PRI508);
1057
+ return { result, capturedPrompt, emitTelemetry };
1058
+ }
1059
+ /** Build a scribe artifact with a custom contentJson string. */
1060
+ function makeScribeWithContentJson(contentJson) {
1061
+ return {
1062
+ artifactId: SCRIBE_ART_ID,
1063
+ artifactKind: 'principle',
1064
+ sourceTaskId: SCRIBE_TASK_ID_PRI508,
1065
+ lineageArtifactIds: [],
1066
+ validationStatus: 'pending',
1067
+ contentJson,
1068
+ createdAt: new Date().toISOString(),
1069
+ updatedAt: new Date().toISOString(),
1070
+ };
1071
+ }
1072
+ /** Build a scribe artifact whose sourceTrace.dreamerArtifactId points to DREAMER_ART_ID. */
1073
+ function makeScribeWithDreamerRef() {
1074
+ return makeScribeWithContentJson(JSON.stringify({
1075
+ taskId: SCRIBE_TASK_ID_PRI508,
1076
+ sourcePhilosopherArtifactId: 'pi-art-philosopher-pri508',
1077
+ principleDraft: { title: 'T', statement: 'S', rationale: 'R', applicability: [], antiPatterns: [], confidence: 0.5 },
1078
+ sourceTrace: { philosopherArtifactId: 'pi-art-philosopher-pri508', dreamerArtifactId: DREAMER_ART_ID },
1079
+ risks: [],
1080
+ generatedAt: new Date().toISOString(),
1081
+ }));
1082
+ }
1083
+ /** Build a dreamer artifact with a custom contentJson string. */
1084
+ function makeDreamerWithContentJson(contentJson) {
1085
+ return {
1086
+ artifactId: DREAMER_ART_ID,
1087
+ artifactKind: 'principle',
1088
+ sourceTaskId: 'dreamer-task-pri508',
1089
+ lineageArtifactIds: [],
1090
+ validationStatus: 'pending',
1091
+ contentJson,
1092
+ createdAt: new Date().toISOString(),
1093
+ updatedAt: new Date().toISOString(),
1094
+ };
1095
+ }
1096
+ it.each([
1097
+ {
1098
+ name: 'scribe contentJson unparseable → emit dreamer_context_skipped',
1099
+ scribeContentJson: '{not valid json',
1100
+ expectedEvent: 'dreamer_context_skipped',
1101
+ },
1102
+ {
1103
+ name: 'scribe contentJson not a record (array) → dreamerContext undefined, no event',
1104
+ scribeContentJson: JSON.stringify(['not', 'a', 'record']),
1105
+ expectedEvent: null,
1106
+ },
1107
+ {
1108
+ name: 'scribe.sourceTrace missing → dreamerContext undefined, no event (backward compat)',
1109
+ scribeContentJson: JSON.stringify({ taskId: SCRIBE_TASK_ID_PRI508, principleDraft: { statement: 'S' } }),
1110
+ expectedEvent: null,
1111
+ },
1112
+ {
1113
+ name: 'scribe.sourceTrace not a record (string) → dreamerContext undefined, no event',
1114
+ scribeContentJson: JSON.stringify({ sourceTrace: 'not-a-record' }),
1115
+ expectedEvent: null,
1116
+ },
1117
+ {
1118
+ name: 'sourceTrace.dreamerArtifactId missing → dreamerContext undefined, no event (backward compat)',
1119
+ scribeContentJson: JSON.stringify({ sourceTrace: { philosopherArtifactId: 'pi-art-philosopher-pri508' } }),
1120
+ expectedEvent: null,
1121
+ },
1122
+ {
1123
+ name: 'sourceTrace.dreamerArtifactId is undefined → dreamerContext undefined, no event',
1124
+ scribeContentJson: JSON.stringify({ sourceTrace: { dreamerArtifactId: undefined } }),
1125
+ expectedEvent: null,
1126
+ },
1127
+ {
1128
+ name: 'sourceTrace.dreamerArtifactId not a string (object) → emit dreamer_context_skipped',
1129
+ scribeContentJson: JSON.stringify({ sourceTrace: { dreamerArtifactId: { evil: true } } }),
1130
+ expectedEvent: 'dreamer_context_skipped',
1131
+ },
1132
+ {
1133
+ name: 'sourceTrace.dreamerArtifactId empty string → emit dreamer_context_skipped',
1134
+ scribeContentJson: JSON.stringify({ sourceTrace: { dreamerArtifactId: '' } }),
1135
+ expectedEvent: 'dreamer_context_skipped',
1136
+ },
1137
+ ])('edge: $name', async ({ scribeContentJson, expectedEvent }) => {
1138
+ const { result, capturedPrompt, emitTelemetry } = await runPri508EdgeCase({
1139
+ scribeArtifact: makeScribeWithContentJson(scribeContentJson),
1140
+ });
1141
+ // Best-effort: runner still succeeds even when dreamer context cannot be resolved.
1142
+ expect(result.status).toBe('succeeded');
1143
+ expect(capturedPrompt).toBeDefined();
1144
+ const parsed = JSON.parse(capturedPrompt);
1145
+ expect(parsed.dreamerContext).toBeUndefined();
1146
+ if (expectedEvent !== null) {
1147
+ // rc-9: failure must be observable, not silent.
1148
+ expect(emitTelemetry).toHaveBeenCalledWith(expect.objectContaining({
1149
+ eventType: expect.stringContaining(expectedEvent),
1150
+ traceId: ARTIFICER_TASK_ID_PRI508,
1151
+ }));
1152
+ }
1153
+ });
1154
+ it.each([
1155
+ {
1156
+ name: 'dreamer contentJson unparseable → emit dreamer_context_invalid',
1157
+ dreamerContentJson: '{broken json',
1158
+ expectedEvent: 'dreamer_context_invalid',
1159
+ },
1160
+ {
1161
+ name: 'dreamer contentJson not a record (array) → emit dreamer_context_invalid',
1162
+ dreamerContentJson: JSON.stringify(['not', 'a', 'record']),
1163
+ expectedEvent: 'dreamer_context_invalid',
1164
+ },
1165
+ {
1166
+ name: 'dreamer candidates missing → emit dreamer_context_invalid',
1167
+ dreamerContentJson: JSON.stringify({ valid: true, taskId: 'dreamer-task-pri508' }),
1168
+ expectedEvent: 'dreamer_context_invalid',
1169
+ },
1170
+ {
1171
+ name: 'dreamer candidates not an array (string) → emit dreamer_context_invalid',
1172
+ dreamerContentJson: JSON.stringify({ candidates: 'not-an-array' }),
1173
+ expectedEvent: 'dreamer_context_invalid',
1174
+ },
1175
+ {
1176
+ name: 'dreamer candidates empty array → emit dreamer_context_invalid',
1177
+ dreamerContentJson: JSON.stringify({ candidates: [] }),
1178
+ expectedEvent: 'dreamer_context_invalid',
1179
+ },
1180
+ {
1181
+ name: 'dreamer candidates[0] not a record (string) → emit dreamer_context_invalid',
1182
+ dreamerContentJson: JSON.stringify({ candidates: ['not-a-record'] }),
1183
+ expectedEvent: 'dreamer_context_invalid',
1184
+ },
1185
+ {
1186
+ name: 'dreamer candidates[0] missing badDecision → emit dreamer_context_invalid',
1187
+ dreamerContentJson: JSON.stringify({ candidates: [{ betterDecision: 'b', rationale: 'r' }] }),
1188
+ expectedEvent: 'dreamer_context_invalid',
1189
+ },
1190
+ {
1191
+ name: 'dreamer candidates[0] missing betterDecision → emit dreamer_context_invalid',
1192
+ dreamerContentJson: JSON.stringify({ candidates: [{ badDecision: 'a', rationale: 'r' }] }),
1193
+ expectedEvent: 'dreamer_context_invalid',
1194
+ },
1195
+ {
1196
+ name: 'dreamer candidates[0] missing rationale → emit dreamer_context_invalid',
1197
+ dreamerContentJson: JSON.stringify({ candidates: [{ badDecision: 'a', betterDecision: 'b' }] }),
1198
+ expectedEvent: 'dreamer_context_invalid',
1199
+ },
1200
+ {
1201
+ name: 'dreamer candidates[0] badDecision empty string → emit dreamer_context_invalid',
1202
+ dreamerContentJson: JSON.stringify({ candidates: [{ badDecision: ' ', betterDecision: 'b', rationale: 'r' }] }),
1203
+ expectedEvent: 'dreamer_context_invalid',
1204
+ },
1205
+ {
1206
+ name: 'dreamer candidates[0] badDecision not a string (number) → emit dreamer_context_invalid',
1207
+ dreamerContentJson: JSON.stringify({ candidates: [{ badDecision: 42, betterDecision: 'b', rationale: 'r' }] }),
1208
+ expectedEvent: 'dreamer_context_invalid',
1209
+ },
1210
+ ])('edge: $name', async ({ dreamerContentJson, expectedEvent }) => {
1211
+ const { result, capturedPrompt, emitTelemetry } = await runPri508EdgeCase({
1212
+ scribeArtifact: makeScribeWithDreamerRef(),
1213
+ dreamerArtifact: makeDreamerWithContentJson(dreamerContentJson),
1214
+ });
1215
+ // Best-effort: runner still succeeds even when dreamer context validation fails.
1216
+ expect(result.status).toBe('succeeded');
1217
+ expect(capturedPrompt).toBeDefined();
1218
+ const parsed = JSON.parse(capturedPrompt);
1219
+ expect(parsed.dreamerContext).toBeUndefined();
1220
+ // rc-9: failure must be observable, not silent.
1221
+ expect(emitTelemetry).toHaveBeenCalledWith(expect.objectContaining({
1222
+ eventType: expect.stringContaining(expectedEvent),
1223
+ traceId: ARTIFICER_TASK_ID_PRI508,
1224
+ }));
1225
+ });
1226
+ it('edge: dreamer candidates[0] with only required 3 fields + no optional → dreamerContext resolved (no riskLevel/strategicPerspective)', async () => {
1227
+ // Validates the optional-field spread path: riskLevel/strategicPerspective absent
1228
+ // should still yield a valid dreamerContext with only the 3 required fields.
1229
+ const dreamerArtifact = makeDreamerWithContentJson(JSON.stringify({
1230
+ candidates: [{
1231
+ badDecision: 'bad',
1232
+ betterDecision: 'better',
1233
+ rationale: 'why',
1234
+ // riskLevel and strategicPerspective intentionally absent
1235
+ }],
1236
+ }));
1237
+ const { result, capturedPrompt } = await runPri508EdgeCase({
1238
+ scribeArtifact: makeScribeWithDreamerRef(),
1239
+ dreamerArtifact,
1240
+ });
1241
+ expect(result.status).toBe('succeeded');
1242
+ expect(capturedPrompt).toBeDefined();
1243
+ const parsed = JSON.parse(capturedPrompt);
1244
+ expect(parsed.dreamerContext).toBeDefined();
1245
+ expect(parsed.dreamerContext.badDecision).toBe('bad');
1246
+ expect(parsed.dreamerContext.betterDecision).toBe('better');
1247
+ expect(parsed.dreamerContext.rationale).toBe('why');
1248
+ expect(parsed.dreamerContext.riskLevel).toBeUndefined();
1249
+ expect(parsed.dreamerContext.strategicPerspective).toBeUndefined();
1250
+ });
1251
+ });
1252
+ // ── PRI-509 Slice 3: ArtificerRunner.buildContext reads repairPayload ────────
1253
+ // When evaluator returns needs_revision, it seeds an artificer repair task
1254
+ // whose diagnosticJson contains a repairPayload. The artificer-runner must
1255
+ // detect this payload, format it as a repairFeedback string, and forward it
1256
+ // to the prompt builder so the LLM addresses each requiredChange instead of
1257
+ // regenerating blind. Trust boundary (rc-1, rc-2): repairPayload originates
1258
+ // from evaluator LLM output persisted in diagnosticJson — already validated
1259
+ // by isValidRepairPayload in pitask-metadata.ts, but the runner must still
1260
+ // treat the hydrated value as opaque text when formatting the prompt.
1261
+ describe('PRI-509: ArtificerRunner.buildContext reads repairPayload → repairFeedback', () => {
1262
+ const SCRIBE_ART_ID_PRI509 = 'pi-art-scribe-pri509';
1263
+ const SCRIBE_TASK_ID_PRI509 = 'scribe-pri509';
1264
+ const ARTIFICER_TASK_ID_PRI509 = 'artificer-repair-r1';
1265
+ function makeScribeArtifactPri509() {
1266
+ return {
1267
+ artifactId: SCRIBE_ART_ID_PRI509,
1268
+ artifactKind: 'principle',
1269
+ sourceTaskId: SCRIBE_TASK_ID_PRI509,
1270
+ lineageArtifactIds: [],
1271
+ validationStatus: 'pending',
1272
+ contentJson: JSON.stringify({
1273
+ taskId: SCRIBE_TASK_ID_PRI509,
1274
+ sourcePhilosopherArtifactId: 'pi-art-philosopher-pri509',
1275
+ principleDraft: {
1276
+ title: 'Input Validation Discipline',
1277
+ statement: 'Validate tool inputs before execution',
1278
+ rationale: 'Unvalidated inputs cause downstream failures',
1279
+ applicability: ['all tools'],
1280
+ antiPatterns: ['Trusting raw input'],
1281
+ confidence: 0.85,
1282
+ },
1283
+ sourceTrace: { philosopherArtifactId: 'pi-art-philosopher-pri509' },
1284
+ risks: [],
1285
+ generatedAt: new Date().toISOString(),
1286
+ }),
1287
+ createdAt: new Date().toISOString(),
1288
+ updatedAt: new Date().toISOString(),
1289
+ };
1290
+ }
1291
+ function makeMetadata(overrides) {
1292
+ return {
1293
+ dependencyTaskIds: overrides?.dependencyTaskIds ?? [],
1294
+ channel: overrides?.channel ?? 'prompt',
1295
+ timeoutMs: overrides?.timeoutMs ?? 300_000,
1296
+ inputArtifactRefs: overrides?.inputArtifactRefs ?? [],
1297
+ outputArtifactRefs: overrides?.outputArtifactRefs ?? [],
1298
+ parentTaskId: overrides?.parentTaskId,
1299
+ correlationId: overrides?.correlationId,
1300
+ rejectionCount: overrides?.rejectionCount,
1301
+ adversarialFeedback: overrides?.adversarialFeedback,
1302
+ repairPayload: overrides?.repairPayload,
1303
+ };
1304
+ }
1305
+ function makeArtificerTaskPri509(repairPayload) {
1306
+ const meta = makeMetadata({
1307
+ dependencyTaskIds: [SCRIBE_TASK_ID_PRI509],
1308
+ channel: 'prompt',
1309
+ timeoutMs: 300_000,
1310
+ inputArtifactRefs: [{ artifactType: 'principle', ref: SCRIBE_ART_ID_PRI509 }],
1311
+ outputArtifactRefs: [],
1312
+ });
1313
+ if (repairPayload) {
1314
+ meta.repairPayload = repairPayload;
1315
+ }
1316
+ return {
1317
+ taskId: ARTIFICER_TASK_ID_PRI509,
1318
+ taskKind: 'artificer',
1319
+ status: 'pending',
1320
+ attemptCount: 0,
1321
+ maxAttempts: 3,
1322
+ createdAt: new Date().toISOString(),
1323
+ updatedAt: new Date().toISOString(),
1324
+ diagnosticJson: createPITaskDiagnosticJson(meta),
1325
+ };
1326
+ }
1327
+ function makeArtificerOutputPri509() {
1328
+ return {
1329
+ taskId: ARTIFICER_TASK_ID_PRI509,
1330
+ sourceScribeArtifactId: SCRIBE_ART_ID_PRI509,
1331
+ implementationCode: 'function evaluate(input, helpers) { return { decision: "allow", matched: false, reason: "ok" }; }',
1332
+ goldenTraceCases: [
1333
+ { caseId: 'positive-1', kind: 'positive', toolName: 'write_file', params: { path: '/workspace/file' }, expectedDecision: 'allow' },
1334
+ { caseId: 'negative-1', kind: 'negative', toolName: 'write_file', params: { path: '/etc/passwd' }, expectedDecision: 'block' },
1335
+ ],
1336
+ affectedTools: ['write_file'],
1337
+ implementationSummary: 'Validate parent path before write',
1338
+ sourceTrace: { scribeArtifactId: SCRIBE_ART_ID_PRI509 },
1339
+ risks: [],
1340
+ generatedAt: new Date().toISOString(),
1341
+ };
1342
+ }
1343
+ function makeScribeTaskPri509() {
1344
+ return {
1345
+ taskId: SCRIBE_TASK_ID_PRI509,
1346
+ taskKind: 'scribe',
1347
+ status: 'succeeded',
1348
+ attemptCount: 1,
1349
+ maxAttempts: 3,
1350
+ resultRef: 'scribe://run-pri509',
1351
+ createdAt: new Date().toISOString(),
1352
+ updatedAt: new Date().toISOString(),
1353
+ diagnosticJson: createPITaskDiagnosticJson({
1354
+ dependencyTaskIds: [],
1355
+ channel: 'prompt',
1356
+ timeoutMs: 300_000,
1357
+ inputArtifactRefs: [],
1358
+ outputArtifactRefs: [{ artifactType: 'principle', ref: SCRIBE_ART_ID_PRI509 }],
1359
+ }),
1360
+ };
1361
+ }
1362
+ async function runPri509RepairLoop(artificerTask) {
1363
+ const store = new MemoryPIArtifactStore();
1364
+ await store.upsertArtifact(makeScribeArtifactPri509());
1365
+ const scribeTask = makeScribeTaskPri509();
1366
+ const stateManager = {
1367
+ acquireLease: vi.fn().mockResolvedValue(artificerTask),
1368
+ getTask: vi.fn().mockImplementation((id) => {
1369
+ if (id === ARTIFICER_TASK_ID_PRI509)
1370
+ return Promise.resolve(artificerTask);
1371
+ if (id === SCRIBE_TASK_ID_PRI509)
1372
+ return Promise.resolve(scribeTask);
1373
+ return Promise.resolve(null);
1374
+ }),
1375
+ getRunsByTask: vi.fn().mockResolvedValue([{ runId: 'run-pri509', taskId: ARTIFICER_TASK_ID_PRI509, runtimeKind: 'artificer', startedAt: new Date().toISOString() }]),
1376
+ getValidRunsByTaskTolerant: vi.fn().mockResolvedValue({ runs: [{ runId: 'run-pri509', taskId: ARTIFICER_TASK_ID_PRI509, runtimeKind: 'artificer', startedAt: new Date().toISOString() }], degradedRuns: [] }),
1377
+ updateRunOutput: vi.fn().mockResolvedValue(undefined),
1378
+ markTaskSucceeded: vi.fn().mockResolvedValue(undefined),
1379
+ markTaskFailed: vi.fn().mockResolvedValue(undefined),
1380
+ markTaskRetryWait: vi.fn().mockResolvedValue(undefined),
1381
+ getRetryPolicy: vi.fn().mockReturnValue({ shouldRetry: () => false }),
1382
+ };
1383
+ let capturedPrompt;
1384
+ const runtimeAdapter = {
1385
+ startRun: vi.fn().mockImplementation((input) => {
1386
+ capturedPrompt = typeof input.inputPayload === 'string' ? input.inputPayload : JSON.stringify(input.inputPayload);
1387
+ return Promise.resolve({ runId: 'run-pri509', runtimeKind: 'test-double', startedAt: new Date().toISOString() });
1388
+ }),
1389
+ pollRun: vi.fn().mockResolvedValue({ status: 'succeeded', runId: 'run-pri509' }),
1390
+ fetchOutput: vi.fn().mockResolvedValue({ payload: makeArtificerOutputPri509() }),
1391
+ cancelRun: vi.fn().mockResolvedValue(undefined),
1392
+ };
1393
+ const eventEmitter = { emitTelemetry: vi.fn() };
1394
+ const validator = new DefaultArtificerValidator();
1395
+ const deps = {
1396
+ stateManager,
1397
+ runtimeAdapter,
1398
+ eventEmitter,
1399
+ artifactStore: store,
1400
+ validator,
1401
+ };
1402
+ const runner = new ArtificerRunner(deps, {
1403
+ owner: 'test',
1404
+ runtimeKind: 'artificer',
1405
+ pollIntervalMs: 10,
1406
+ timeoutMs: 1000,
1407
+ });
1408
+ const result = await runner.run(ARTIFICER_TASK_ID_PRI509);
1409
+ return { result, capturedPrompt };
1410
+ }
1411
+ it('repairPayload present → repairFeedback serialized into prompt message (with requiredChanges text + previousScore)', async () => {
1412
+ const artificerTask = makeArtificerTaskPri509({
1413
+ requiredChanges: ['add path validation', 'fix case sensitivity'],
1414
+ concerns: ['code quality is poor', 'missing error handling'],
1415
+ previousScore: 0.65,
1416
+ repairIteration: 1,
1417
+ sourceArtificerArtifactId: 'pi-art-artificer-original',
1418
+ sourceEvaluatorTaskId: 'evaluator-r0',
1419
+ });
1420
+ const { result, capturedPrompt } = await runPri509RepairLoop(artificerTask);
1421
+ expect(result.status).toBe('succeeded');
1422
+ expect(capturedPrompt).toBeDefined();
1423
+ const parsed = JSON.parse(capturedPrompt);
1424
+ expect(parsed.repairFeedback).toBeDefined();
1425
+ // requiredChanges text must be present so artificer can address each one
1426
+ expect(parsed.repairFeedback).toContain('add path validation');
1427
+ expect(parsed.repairFeedback).toContain('fix case sensitivity');
1428
+ // previousScore must be present so artificer knows the prior score
1429
+ expect(parsed.repairFeedback).toContain('0.65');
1430
+ // concerns text must be present (PoC-validated format)
1431
+ expect(parsed.repairFeedback).toContain('code quality is poor');
1432
+ // Must signal this is a needs_revision retry (not a fresh attempt)
1433
+ expect(parsed.repairFeedback).toMatch(/needs_revision|prior attempt/i);
1434
+ });
1435
+ it('repairPayload absent → repairFeedback undefined in prompt (backward compat)', async () => {
1436
+ // Round-1 artificer task without repairPayload — repairFeedback must NOT
1437
+ // appear in the prompt (backward compatible with pre-PRI-509 flows).
1438
+ const artificerTask = makeArtificerTaskPri509(); // no repairPayload
1439
+ const { result, capturedPrompt } = await runPri509RepairLoop(artificerTask);
1440
+ expect(result.status).toBe('succeeded');
1441
+ expect(capturedPrompt).toBeDefined();
1442
+ const parsed = JSON.parse(capturedPrompt);
1443
+ expect(parsed.repairFeedback).toBeUndefined();
1444
+ });
1445
+ it('repairPayload present → repairFeedback appears in promptInput (typed surface)', async () => {
1446
+ const artificerTask = makeArtificerTaskPri509({
1447
+ requiredChanges: ['add input validation'],
1448
+ concerns: [],
1449
+ previousScore: 0.55,
1450
+ repairIteration: 2,
1451
+ sourceArtificerArtifactId: 'pi-art-artificer-r1',
1452
+ sourceEvaluatorTaskId: 'evaluator-r1',
1453
+ });
1454
+ const { result } = await runPri509RepairLoop(artificerTask);
1455
+ expect(result.status).toBe('succeeded');
1456
+ // The typed promptInput surface must also carry repairFeedback ( Slice 2 contract).
1457
+ // We can't directly inspect promptInput here — it's internal to the runner.
1458
+ // The prompt message assertion above already covers the e2e path. This test
1459
+ // exists to assert the runner doesn't throw when repairIteration=2.
1460
+ });
1461
+ it('repairPayload + adversarialFeedback both present → both forwarded (orthogonal)', async () => {
1462
+ // PRI-428 (adversarialFeedback) and PRI-509 (repairPayload) are orthogonal
1463
+ // signals — adversarialFeedback comes from RuleHost probe failures, while
1464
+ // repairPayload comes from evaluator needs_revision. Both can be present on
1465
+ // the same task and must both reach the prompt.
1466
+ const meta = makeMetadata({
1467
+ dependencyTaskIds: [SCRIBE_TASK_ID_PRI509],
1468
+ channel: 'prompt',
1469
+ timeoutMs: 300_000,
1470
+ inputArtifactRefs: [{ artifactType: 'principle', ref: SCRIBE_ART_ID_PRI509 }],
1471
+ outputArtifactRefs: [],
1472
+ });
1473
+ meta.adversarialFeedback = 'Adversarial probe failed: tool=test_tool, case=neg-1, expected=block, got=allow';
1474
+ meta.repairPayload = {
1475
+ requiredChanges: ['add input validation'],
1476
+ concerns: ['missing error handling'],
1477
+ previousScore: 0.6,
1478
+ repairIteration: 1,
1479
+ sourceArtificerArtifactId: 'pi-art-artificer-original',
1480
+ sourceEvaluatorTaskId: 'evaluator-r0',
1481
+ };
1482
+ const artificerTask = {
1483
+ taskId: ARTIFICER_TASK_ID_PRI509,
1484
+ taskKind: 'artificer',
1485
+ status: 'pending',
1486
+ attemptCount: 0,
1487
+ maxAttempts: 3,
1488
+ createdAt: new Date().toISOString(),
1489
+ updatedAt: new Date().toISOString(),
1490
+ diagnosticJson: createPITaskDiagnosticJson(meta),
1491
+ };
1492
+ const { result, capturedPrompt } = await runPri509RepairLoop(artificerTask);
1493
+ expect(result.status).toBe('succeeded');
1494
+ expect(capturedPrompt).toBeDefined();
1495
+ const parsed = JSON.parse(capturedPrompt);
1496
+ // Both signals must be present
1497
+ expect(parsed.adversarialFeedback).toBeDefined();
1498
+ expect(parsed.adversarialFeedback).toContain('Adversarial probe failed');
1499
+ expect(parsed.repairFeedback).toBeDefined();
1500
+ expect(parsed.repairFeedback).toContain('add input validation');
1501
+ expect(parsed.repairFeedback).toContain('0.6');
1502
+ });
1503
+ });
665
1504
  //# sourceMappingURL=artificer-runner-vslice.test.js.map