iterate-plugin 3.2.1 → 3.3.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/lib/parse.js CHANGED
@@ -557,12 +557,15 @@ export function normalizeTranscript(manifest) {
557
557
  : null
558
558
 
559
559
  const ap = safeGet(src, 'approval')
560
+ const tm = safeGet(src, 'taskMode')
560
561
  return {
561
562
  version: asNum(safeGet(src, 'version')),
562
563
  project: asStr(safeGet(src, 'project')),
563
564
  updatedAt: asStr(safeGet(src, 'updatedAt')),
564
565
  active: asBool(safeGet(src, 'active')),
565
566
  mode: asStr(safeGet(src, 'mode')) || null,
567
+ // v3.0: harness task_mode (code/iterate), tolerated when absent.
568
+ taskMode: tm === 'code' || tm === 'iterate' ? tm : null,
566
569
  goal: asStr(safeGet(src, 'goal')),
567
570
  phases: asArray(safeGet(src, 'phases')).map((p) => (typeof p === 'string' ? p : '')),
568
571
  round: asNum(safeGet(src, 'round')),
@@ -586,6 +589,355 @@ export function normalizeTranscript(manifest) {
586
589
  }
587
590
  }
588
591
 
592
+ // ─── Quality command center session scans (v3.1+) ───────────────────────────
593
+ //
594
+ // The F8/F9/F10 observatory tabs render machine-readable data that the model
595
+ // already surfaces through `iterate_quality_gate` / `iterate_experience` /
596
+ // `iterate_defense_events`. Because the client cannot call harness tools, these
597
+ // scanners recover the LATEST result of each tool from the in-memory session
598
+ // stream (same reverse-chronological strategy as scanSessionForTranscript) and
599
+ // normalize it into a JSON-safe shape for rendering. Every field degrades to a
600
+ // safe default so a partial/malformed result never crashes the slot.
601
+
602
+ const DEFENSE_EVENT_TYPES = ['precondition_failed', 'rollback', 'invariant_violated', 'assumption_falsified']
603
+
604
+ /**
605
+ * Check whether `obj` is a QualityGateSnapshot as produced by quality-store.ts.
606
+ * Requires the three gate discriminators (overallStatus + overallScore +
607
+ * dimensions array) so unrelated `{ snapshot: ... }` shapes never collide.
608
+ *
609
+ * @param {unknown} obj
610
+ * @returns {boolean}
611
+ */
612
+ export function isQualityGateSnapshot(obj) {
613
+ if (!obj || typeof obj !== 'object') return false
614
+ const o = /** @type {Record<string, unknown>} */ (obj)
615
+ const status = safeGet(o, 'overallStatus')
616
+ return (
617
+ (status === 'pass' || status === 'fail' || status === 'pending') &&
618
+ typeof safeGet(o, 'overallScore') === 'number' &&
619
+ Array.isArray(safeGet(o, 'dimensions'))
620
+ )
621
+ }
622
+
623
+ /**
624
+ * Check whether `obj` is an `iterate_experience` result node. `list`/`search`
625
+ * carry an `entries` array; `get`/`add` carry a single `entry` object. Either
626
+ * shape is enough to render the F9 experience bank.
627
+ *
628
+ * @param {unknown} obj
629
+ * @returns {boolean}
630
+ */
631
+ export function isExperienceBankResult(obj) {
632
+ if (!obj || typeof obj !== 'object') return false
633
+ const o = /** @type {Record<string, unknown>} */ (obj)
634
+ const entry = safeGet(o, 'entry')
635
+ return (
636
+ (Array.isArray(safeGet(o, 'entries')) && typeof safeGet(o, 'count') === 'number') ||
637
+ (!!entry && typeof entry === 'object' && typeof safeGet(o, 'operation') === 'string')
638
+ )
639
+ }
640
+
641
+ /**
642
+ * Check whether `obj` is an `iterate_defense_events` result node. `list` carries
643
+ * an `events` array, `counts` / `record` carry a `counts` map. Either shape is
644
+ * enough to render the F10 defense event stream.
645
+ *
646
+ * @param {unknown} obj
647
+ * @returns {boolean}
648
+ */
649
+ export function isDefenseEventsResult(obj) {
650
+ if (!obj || typeof obj !== 'object') return false
651
+ const o = /** @type {Record<string, unknown>} */ (obj)
652
+ const counts = safeGet(o, 'counts')
653
+ return (
654
+ (Array.isArray(safeGet(o, 'events')) && typeof safeGet(o, 'count') === 'number') ||
655
+ (!!counts && typeof counts === 'object' && typeof safeGet(o, 'operation') === 'string')
656
+ )
657
+ }
658
+
659
+ /**
660
+ * Shared deep-find: first node in `obj` (circular + depth guarded) satisfying a
661
+ * predicate, with the same traversal semantics as findReportInObject.
662
+ *
663
+ * @param {unknown} obj
664
+ * @param {(o: Record<string, unknown>) => boolean} predicate
665
+ * @param {Set<unknown>} [seen]
666
+ * @param {number} [maxDepth=20]
667
+ * @returns {Record<string, unknown> | null}
668
+ */
669
+ function findFirstInObject(obj, predicate, seen, maxDepth = 20) {
670
+ if (maxDepth <= 0) return null
671
+ if (!obj || typeof obj !== 'object') return null
672
+
673
+ const s = seen || new Set()
674
+ if (s.has(obj)) return null
675
+ s.add(obj)
676
+
677
+ if (Array.isArray(obj)) {
678
+ for (const item of obj) {
679
+ const found = findFirstInObject(item, predicate, s, maxDepth - 1)
680
+ if (found) return found
681
+ }
682
+ return null
683
+ }
684
+
685
+ if (typeof obj === 'object') {
686
+ const o = /** @type {Record<string, unknown>} */ (obj)
687
+ if (predicate(o)) return o
688
+ for (const key of safeKeys(o)) {
689
+ const val = safeGet(o, key)
690
+ if (val && typeof val === 'object') {
691
+ const found = findFirstInObject(val, predicate, s, maxDepth - 1)
692
+ if (found) return found
693
+ }
694
+ }
695
+ }
696
+ return null
697
+ }
698
+
699
+ /** Deep-find the first QualityGateSnapshot inside `obj`. */
700
+ function findQualityGateInObject(obj) {
701
+ return findFirstInObject(obj, (o) => isQualityGateSnapshot(o))
702
+ }
703
+
704
+ /** Deep-find the first `iterate_experience` result node inside `obj`. */
705
+ function findExperienceResultInObject(obj) {
706
+ return findFirstInObject(obj, (o) => isExperienceBankResult(o))
707
+ }
708
+
709
+ /** Deep-find the first `iterate_defense_events` result node inside `obj`. */
710
+ function findDefenseResultInObject(obj) {
711
+ return findFirstInObject(obj, (o) => isDefenseEventsResult(o))
712
+ }
713
+
714
+ /**
715
+ * Return the raw result/message node of the most recent execution of `toolName`
716
+ * in a session snapshot. Scans `session.toolCalls` (reverse chronological,
717
+ * matching the harness's in-memory stream shape) and falls back to
718
+ * `session.messages[].content` (assistant tool-call blocks). Returns the raw
719
+ * node (object or string) or null.
720
+ *
721
+ * @param {unknown} session
722
+ * @param {string} toolName
723
+ * @returns {unknown}
724
+ */
725
+ function latestToolResultNode(session, toolName) {
726
+ if (!session || typeof session !== 'object') return null
727
+
728
+ const s = /** @type {Record<string, unknown>} */ (session)
729
+
730
+ const toolCalls = safeGet(s, 'toolCalls')
731
+ if (Array.isArray(toolCalls)) {
732
+ const calls = /** @type {Array<Record<string, unknown>>} */ (toolCalls)
733
+ for (let i = calls.length - 1; i >= 0; i--) {
734
+ const call = calls[i]
735
+ if (!call) continue
736
+ const tool = String(safeGet(call, 'tool') ?? '')
737
+ if (tool !== toolName && !tool.endsWith(toolName)) continue
738
+ const result = safeGet(call, 'result')
739
+ if (result !== undefined && result !== null) return result
740
+ const message = safeGet(call, 'message')
741
+ if (message !== undefined && message !== null) return message
742
+ }
743
+ }
744
+
745
+ const messages = safeGet(s, 'messages')
746
+ if (Array.isArray(messages)) {
747
+ const msgs = /** @type {Array<Record<string, unknown>>} */ (messages)
748
+ for (let i = msgs.length - 1; i >= 0; i--) {
749
+ const msg = msgs[i]
750
+ if (!msg) continue
751
+ const content = safeGet(msg, 'content')
752
+ if (content !== undefined && content !== null) return content
753
+ }
754
+ }
755
+
756
+ return null
757
+ }
758
+
759
+ /**
760
+ * Normalize a QualityGateSnapshot into a JSON-safe object (see
761
+ * normalizeTranscript for the defensive style contract).
762
+ *
763
+ * @param {Record<string, unknown> | null | undefined} raw
764
+ * @returns {Record<string, unknown>}
765
+ */
766
+ export function normalizeQualityGateSnapshot(raw) {
767
+ const src = raw && typeof raw === 'object'
768
+ ? /** @type {Record<string, unknown>} */ (raw)
769
+ : {}
770
+ const asNum = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : 0)
771
+ const asStr = (v) => (typeof v === 'string' ? v : '')
772
+ const status = safeGet(src, 'overallStatus')
773
+ const dims = Array.isArray(safeGet(src, 'dimensions')) ? safeGet(src, 'dimensions') : []
774
+ return {
775
+ timestamp: asStr(safeGet(src, 'timestamp')),
776
+ overallStatus: status === 'pass' || status === 'fail' || status === 'pending' ? status : 'pending',
777
+ overallScore: asNum(safeGet(src, 'overallScore')),
778
+ verificationPassRate: asNum(safeGet(src, 'verificationPassRate')),
779
+ totalChecks: asNum(safeGet(src, 'totalChecks')),
780
+ passedChecks: asNum(safeGet(src, 'passedChecks')),
781
+ failedChecks: asNum(safeGet(src, 'failedChecks')),
782
+ failReason: asStr(safeGet(src, 'failReason')) || null,
783
+ totalFindings: asNum(safeGet(src, 'totalFindings')),
784
+ criticalCount: asNum(safeGet(src, 'criticalCount')),
785
+ highCount: asNum(safeGet(src, 'highCount')),
786
+ mediumCount: asNum(safeGet(src, 'mediumCount')),
787
+ lowCount: asNum(safeGet(src, 'lowCount')),
788
+ dimensions: (/** @type {unknown[]} */ (dims)).map((d) => {
789
+ const rec = /** @type {Record<string, unknown>} */ (d && typeof d === 'object' ? d : {})
790
+ const dimStatus = safeGet(rec, 'status')
791
+ return {
792
+ dimension: asStr(safeGet(rec, 'dimension')),
793
+ convergenceRate: asNum(safeGet(rec, 'convergenceRate')),
794
+ findingsCount: asNum(safeGet(rec, 'findingsCount')),
795
+ fixedCount: asNum(safeGet(rec, 'fixedCount')),
796
+ score: asNum(safeGet(rec, 'score')),
797
+ status: dimStatus === 'pass' || dimStatus === 'warn' || dimStatus === 'fail' ? dimStatus : 'warn',
798
+ }
799
+ }),
800
+ }
801
+ }
802
+
803
+ /**
804
+ * Scan a session snapshot for the latest `iterate_quality_gate` result and
805
+ * return its normalized QualityGateSnapshot (or null).
806
+ *
807
+ * @param {unknown} session
808
+ * @returns {Record<string, unknown> | null}
809
+ */
810
+ export function scanSessionForQualityGate(session) {
811
+ const node = latestToolResultNode(session, 'iterate_quality_gate')
812
+ if (node === null) return null
813
+ const found = findQualityGateInObject(node)
814
+ if (!found) return null
815
+ return normalizeQualityGateSnapshot(found)
816
+ }
817
+
818
+ /**
819
+ * Normalize an `iterate_experience` result node into a JSON-safe object.
820
+ * `get`/`add` results (single `entry`) are folded into the `entries` array so
821
+ * the F9 panel has one rendering path regardless of operation.
822
+ *
823
+ * @param {Record<string, unknown> | null | undefined} raw
824
+ * @returns {Record<string, unknown>}
825
+ */
826
+ export function normalizeExperienceBankResult(raw) {
827
+ const src = raw && typeof raw === 'object'
828
+ ? /** @type {Record<string, unknown>} */ (raw)
829
+ : {}
830
+ const asNum = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : 0)
831
+ const asStr = (v) => (typeof v === 'string' ? v : '')
832
+ const asArr = (v) => (Array.isArray(v) ? /** @type {unknown[]} */ (v) : [])
833
+ const entry = safeGet(src, 'entry')
834
+ const rawEntries = Array.isArray(safeGet(src, 'entries'))
835
+ ? /** @type {unknown[]} */ (safeGet(src, 'entries'))
836
+ : (entry && typeof entry === 'object' ? [entry] : [])
837
+ return {
838
+ operation: asStr(safeGet(src, 'operation')),
839
+ count: asNum(safeGet(src, 'count')),
840
+ totalHits: asNum(safeGet(src, 'totalHits')),
841
+ added: safeGet(src, 'added') === true,
842
+ entries: rawEntries.map((e) => {
843
+ const rec = /** @type {Record<string, unknown>} */ (e && typeof e === 'object' ? e : {})
844
+ return {
845
+ id: asStr(safeGet(rec, 'id')),
846
+ timestamp: asStr(safeGet(rec, 'timestamp')),
847
+ dimension: asStr(safeGet(rec, 'dimension')),
848
+ pattern: asStr(safeGet(rec, 'pattern')),
849
+ description: asStr(safeGet(rec, 'description')),
850
+ verifiedFix: asStr(safeGet(rec, 'verifiedFix')),
851
+ findingSummary: asStr(safeGet(rec, 'findingSummary')),
852
+ severity: asStr(safeGet(rec, 'severity')),
853
+ hitCount: asNum(safeGet(rec, 'hitCount')),
854
+ lastHitAt: asStr(safeGet(rec, 'lastHitAt')) || null,
855
+ files: asArr(safeGet(rec, 'files')).map((f) => (typeof f === 'string' ? f : '')),
856
+ tags: asArr(safeGet(rec, 'tags')).map((t) => (typeof t === 'string' ? t : '')),
857
+ }
858
+ }),
859
+ }
860
+ }
861
+
862
+ /**
863
+ * Scan a session snapshot for the latest `iterate_experience` result and return
864
+ * its normalized values (or null when the session has none).
865
+ *
866
+ * @param {unknown} session
867
+ * @returns {Record<string, unknown> | null}
868
+ */
869
+ export function scanSessionForExperienceBank(session) {
870
+ const node = latestToolResultNode(session, 'iterate_experience')
871
+ if (node === null) return null
872
+ const found = findExperienceResultInObject(node)
873
+ if (!found) return null
874
+ return normalizeExperienceBankResult(found)
875
+ }
876
+
877
+ /**
878
+ * Normalize an `iterate_defense_events` result node into a JSON-safe object.
879
+ * `record` results (single `event` + `counts`) fold into the same shape as
880
+ * `list`/`counts` so the F10 panel has one rendering path.
881
+ *
882
+ * @param {Record<string, unknown> | null | undefined} raw
883
+ * @returns {Record<string, unknown>}
884
+ */
885
+ export function normalizeDefenseEventsResult(raw) {
886
+ const src = raw && typeof raw === 'object'
887
+ ? /** @type {Record<string, unknown>} */ (raw)
888
+ : {}
889
+ const asNum = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : 0)
890
+ const asStr = (v) => (typeof v === 'string' ? v : '')
891
+ const countsRaw = safeGet(src, 'counts') && typeof safeGet(src, 'counts') === 'object'
892
+ ? /** @type {Record<string, unknown>} */ (safeGet(src, 'counts'))
893
+ : {}
894
+ const counts = /** @type {Record<string, number>} */ ({})
895
+ for (const type of DEFENSE_EVENT_TYPES) {
896
+ const n = safeGet(countsRaw, type)
897
+ counts[type] = typeof n === 'number' && Number.isFinite(n) && n >= 0 ? n : 0
898
+ }
899
+ const event = safeGet(src, 'event')
900
+ const rawEvents = Array.isArray(safeGet(src, 'events'))
901
+ ? /** @type {unknown[]} */ (safeGet(src, 'events'))
902
+ : (event && typeof event === 'object' ? [event] : [])
903
+ return {
904
+ operation: asStr(safeGet(src, 'operation')),
905
+ count: asNum(safeGet(src, 'count')),
906
+ language: asStr(safeGet(src, 'language')),
907
+ counts,
908
+ events: rawEvents.map((e) => {
909
+ const rec = /** @type {Record<string, unknown>} */ (e && typeof e === 'object' ? e : {})
910
+ return {
911
+ id: asStr(safeGet(rec, 'id')),
912
+ timestamp: asStr(safeGet(rec, 'timestamp')),
913
+ round: asNum(safeGet(rec, 'round')),
914
+ type: asStr(safeGet(rec, 'type')),
915
+ description: asStr(safeGet(rec, 'description')),
916
+ defense: asStr(safeGet(rec, 'defense')),
917
+ outcome: asStr(safeGet(rec, 'outcome')),
918
+ file: asStr(safeGet(rec, 'file')) || null,
919
+ line: asNum(safeGet(rec, 'line')) || null,
920
+ severity: asStr(safeGet(rec, 'severity')),
921
+ }
922
+ }),
923
+ }
924
+ }
925
+
926
+ /**
927
+ * Scan a session snapshot for the latest `iterate_defense_events` result and
928
+ * return its normalized values (or null when the session has none).
929
+ *
930
+ * @param {unknown} session
931
+ * @returns {Record<string, unknown> | null}
932
+ */
933
+ export function scanSessionForDefenseEvents(session) {
934
+ const node = latestToolResultNode(session, 'iterate_defense_events')
935
+ if (node === null) return null
936
+ const found = findDefenseResultInObject(node)
937
+ if (!found) return null
938
+ return normalizeDefenseEventsResult(found)
939
+ }
940
+
589
941
  // ─── Run-summary / meta-review verdict detection ─────────────────────────────
590
942
 
591
943
  /**
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "iterate-plugin",
3
- "version": "3.2.1",
4
- "description": "dsh plugin that turns the iterate skill into an autonomous closed-loop harness with quality command center and experience bank (v3.2). Features: plan -> parallel review xN -> atomic fixes -> validate -> loop -> auto-stop, plus dry-run pure-review mode, quality gate compute/persist, writable experience bank, defense events stream (record + bilingual labels), and native command buttons.",
3
+ "version": "3.3.0",
4
+ "description": "dsh plugin that turns the iterate skill into an autonomous closed-loop harness with quality command center and experience bank (v3.3). Features: plan -> parallel review xN -> atomic fixes -> validate -> loop -> auto-stop, plus dry-run pure-review mode, quality gate compute/persist, writable experience bank, defense events stream (record + bilingual labels), and native command buttons — with a live quality command center (F8/F9/F10 render real session data), §8 assign-fix instruction, and task_mode indicator wired end to end.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "repository": {
@@ -102,30 +102,4 @@ export function decideApproval(
102
102
  /** True when any destructive iterate tool is listed in a name set. */
103
103
  export function isDestructiveIterateTool(name: unknown): boolean {
104
104
  return typeof name === 'string' && DESTRUCTIVE_TOOLS.has(name)
105
- }
106
-
107
- /** Tool-facing gate result: run, or refuse (with an explicit human-approval signal). */
108
- export type ToolGateResult =
109
- | { ok: true }
110
- | { ok: false; requiresApproval: true; reason: string }
111
- | { ok: false; error: string }
112
-
113
- /**
114
- * Evaluate an iterate tool's own boundary gate for a destructive call.
115
- * `approvedArg` is the caller-supplied `approved: true` flag (human consent
116
- * already obtained). Returns a run / refuse result without any I/O.
117
- */
118
- export function toolGate(
119
- policy: 'ask' | 'deny' | 'allow',
120
- execution: ToolExecutionLike,
121
- approvedArg?: unknown,
122
- ): ToolGateResult {
123
- const decision = decideApproval(execution, policy)
124
- if (decision.kind === 'allow') return { ok: true }
125
- if (decision.kind === 'deny') {
126
- return { ok: false, error: `Blocked by observatory approval policy: ${decision.reason}` }
127
- }
128
- // ask
129
- if (approvedArg === true) return { ok: true }
130
- return { ok: false, requiresApproval: true, reason: decision.reason }
131
105
  }