instar 0.26.11 → 0.27.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 (161) hide show
  1. package/README.md +1 -1
  2. package/dist/commands/init.d.ts +5 -0
  3. package/dist/commands/init.d.ts.map +1 -1
  4. package/dist/commands/init.js +46 -1
  5. package/dist/commands/init.js.map +1 -1
  6. package/dist/commands/server.d.ts.map +1 -1
  7. package/dist/commands/server.js +228 -36
  8. package/dist/commands/server.js.map +1 -1
  9. package/dist/commands/slack-cli.d.ts +6 -0
  10. package/dist/commands/slack-cli.d.ts.map +1 -1
  11. package/dist/commands/slack-cli.js +99 -1
  12. package/dist/commands/slack-cli.js.map +1 -1
  13. package/dist/core/GitSync.d.ts.map +1 -1
  14. package/dist/core/GitSync.js +47 -5
  15. package/dist/core/GitSync.js.map +1 -1
  16. package/dist/core/PostUpdateMigrator.d.ts +7 -1
  17. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  18. package/dist/core/PostUpdateMigrator.js +19 -1
  19. package/dist/core/PostUpdateMigrator.js.map +1 -1
  20. package/dist/core/types.d.ts +9 -0
  21. package/dist/core/types.d.ts.map +1 -1
  22. package/dist/identity/IdentityManager.d.ts +70 -0
  23. package/dist/identity/IdentityManager.d.ts.map +1 -0
  24. package/dist/identity/IdentityManager.js +179 -0
  25. package/dist/identity/IdentityManager.js.map +1 -0
  26. package/dist/identity/KeyEncryption.d.ts +40 -0
  27. package/dist/identity/KeyEncryption.d.ts.map +1 -0
  28. package/dist/identity/KeyEncryption.js +93 -0
  29. package/dist/identity/KeyEncryption.js.map +1 -0
  30. package/dist/identity/KeyRevocation.d.ts +68 -0
  31. package/dist/identity/KeyRevocation.d.ts.map +1 -0
  32. package/dist/identity/KeyRevocation.js +171 -0
  33. package/dist/identity/KeyRevocation.js.map +1 -0
  34. package/dist/identity/KeyRotation.d.ts +43 -0
  35. package/dist/identity/KeyRotation.d.ts.map +1 -0
  36. package/dist/identity/KeyRotation.js +81 -0
  37. package/dist/identity/KeyRotation.js.map +1 -0
  38. package/dist/identity/Migration.d.ts +50 -0
  39. package/dist/identity/Migration.d.ts.map +1 -0
  40. package/dist/identity/Migration.js +125 -0
  41. package/dist/identity/Migration.js.map +1 -0
  42. package/dist/identity/RecoveryPhrase.d.ts +43 -0
  43. package/dist/identity/RecoveryPhrase.d.ts.map +1 -0
  44. package/dist/identity/RecoveryPhrase.js +93 -0
  45. package/dist/identity/RecoveryPhrase.js.map +1 -0
  46. package/dist/identity/index.d.ts +13 -0
  47. package/dist/identity/index.d.ts.map +1 -0
  48. package/dist/identity/index.js +20 -0
  49. package/dist/identity/index.js.map +1 -0
  50. package/dist/identity/types.d.ts +101 -0
  51. package/dist/identity/types.d.ts.map +1 -0
  52. package/dist/identity/types.js +55 -0
  53. package/dist/identity/types.js.map +1 -0
  54. package/dist/knowledge/TreeTriage.d.ts.map +1 -1
  55. package/dist/knowledge/TreeTriage.js +9 -0
  56. package/dist/knowledge/TreeTriage.js.map +1 -1
  57. package/dist/memory/TopicMemory.d.ts +2 -0
  58. package/dist/memory/TopicMemory.d.ts.map +1 -1
  59. package/dist/memory/TopicMemory.js +55 -0
  60. package/dist/memory/TopicMemory.js.map +1 -1
  61. package/dist/messaging/slack/SlackAdapter.d.ts +15 -4
  62. package/dist/messaging/slack/SlackAdapter.d.ts.map +1 -1
  63. package/dist/messaging/slack/SlackAdapter.js +131 -14
  64. package/dist/messaging/slack/SlackAdapter.js.map +1 -1
  65. package/dist/messaging/slack/SocketModeClient.d.ts +2 -0
  66. package/dist/messaging/slack/SocketModeClient.d.ts.map +1 -1
  67. package/dist/messaging/slack/SocketModeClient.js +31 -1
  68. package/dist/messaging/slack/SocketModeClient.js.map +1 -1
  69. package/dist/moltbridge/MoltBridgeClient.d.ts +124 -0
  70. package/dist/moltbridge/MoltBridgeClient.d.ts.map +1 -0
  71. package/dist/moltbridge/MoltBridgeClient.js +321 -0
  72. package/dist/moltbridge/MoltBridgeClient.js.map +1 -0
  73. package/dist/moltbridge/ProfileCompiler.d.ts +68 -0
  74. package/dist/moltbridge/ProfileCompiler.d.ts.map +1 -0
  75. package/dist/moltbridge/ProfileCompiler.js +317 -0
  76. package/dist/moltbridge/ProfileCompiler.js.map +1 -0
  77. package/dist/moltbridge/index.d.ts +12 -0
  78. package/dist/moltbridge/index.d.ts.map +1 -0
  79. package/dist/moltbridge/index.js +11 -0
  80. package/dist/moltbridge/index.js.map +1 -0
  81. package/dist/moltbridge/routes.d.ts +21 -0
  82. package/dist/moltbridge/routes.d.ts.map +1 -0
  83. package/dist/moltbridge/routes.js +224 -0
  84. package/dist/moltbridge/routes.js.map +1 -0
  85. package/dist/moltbridge/types.d.ts +93 -0
  86. package/dist/moltbridge/types.d.ts.map +1 -0
  87. package/dist/moltbridge/types.js +20 -0
  88. package/dist/moltbridge/types.js.map +1 -0
  89. package/dist/monitoring/PresenceProxy.d.ts +3 -0
  90. package/dist/monitoring/PresenceProxy.d.ts.map +1 -1
  91. package/dist/monitoring/PresenceProxy.js +80 -3
  92. package/dist/monitoring/PresenceProxy.js.map +1 -1
  93. package/dist/monitoring/SessionMonitor.d.ts +7 -0
  94. package/dist/monitoring/SessionMonitor.d.ts.map +1 -1
  95. package/dist/monitoring/SessionMonitor.js +47 -22
  96. package/dist/monitoring/SessionMonitor.js.map +1 -1
  97. package/dist/monitoring/TriageOrchestrator.d.ts.map +1 -1
  98. package/dist/monitoring/TriageOrchestrator.js +25 -1
  99. package/dist/monitoring/TriageOrchestrator.js.map +1 -1
  100. package/dist/scaffold/templates.d.ts.map +1 -1
  101. package/dist/scaffold/templates.js +29 -1
  102. package/dist/scaffold/templates.js.map +1 -1
  103. package/dist/server/AgentServer.d.ts +1 -0
  104. package/dist/server/AgentServer.d.ts.map +1 -1
  105. package/dist/server/AgentServer.js +1 -0
  106. package/dist/server/AgentServer.js.map +1 -1
  107. package/dist/server/routes.d.ts +2 -0
  108. package/dist/server/routes.d.ts.map +1 -1
  109. package/dist/server/routes.js +21 -0
  110. package/dist/server/routes.js.map +1 -1
  111. package/dist/threadline/AuthorizationPolicy.d.ts +110 -0
  112. package/dist/threadline/AuthorizationPolicy.d.ts.map +1 -0
  113. package/dist/threadline/AuthorizationPolicy.js +226 -0
  114. package/dist/threadline/AuthorizationPolicy.js.map +1 -0
  115. package/dist/threadline/DiscoveryWaterfall.d.ts +73 -0
  116. package/dist/threadline/DiscoveryWaterfall.d.ts.map +1 -0
  117. package/dist/threadline/DiscoveryWaterfall.js +113 -0
  118. package/dist/threadline/DiscoveryWaterfall.js.map +1 -0
  119. package/dist/threadline/ListenerSessionManager.d.ts.map +1 -1
  120. package/dist/threadline/ListenerSessionManager.js +2 -0
  121. package/dist/threadline/ListenerSessionManager.js.map +1 -1
  122. package/dist/threadline/MessageSecurity.d.ts +49 -0
  123. package/dist/threadline/MessageSecurity.d.ts.map +1 -0
  124. package/dist/threadline/MessageSecurity.js +93 -0
  125. package/dist/threadline/MessageSecurity.js.map +1 -0
  126. package/dist/threadline/SecureInvitation.d.ts +79 -0
  127. package/dist/threadline/SecureInvitation.d.ts.map +1 -0
  128. package/dist/threadline/SecureInvitation.js +164 -0
  129. package/dist/threadline/SecureInvitation.js.map +1 -0
  130. package/dist/threadline/TrustAuditLog.d.ts +49 -0
  131. package/dist/threadline/TrustAuditLog.d.ts.map +1 -0
  132. package/dist/threadline/TrustAuditLog.js +111 -0
  133. package/dist/threadline/TrustAuditLog.js.map +1 -0
  134. package/dist/threadline/TrustEvaluator.d.ts +69 -0
  135. package/dist/threadline/TrustEvaluator.d.ts.map +1 -0
  136. package/dist/threadline/TrustEvaluator.js +93 -0
  137. package/dist/threadline/TrustEvaluator.js.map +1 -0
  138. package/dist/threadline/UnifiedTrustWiring.d.ts +66 -0
  139. package/dist/threadline/UnifiedTrustWiring.d.ts.map +1 -0
  140. package/dist/threadline/UnifiedTrustWiring.js +192 -0
  141. package/dist/threadline/UnifiedTrustWiring.js.map +1 -0
  142. package/dist/threadline/client/IdentityManager.d.ts +27 -6
  143. package/dist/threadline/client/IdentityManager.d.ts.map +1 -1
  144. package/dist/threadline/client/IdentityManager.js +64 -18
  145. package/dist/threadline/client/IdentityManager.js.map +1 -1
  146. package/dist/threadline/relay/SybilProtection.d.ts +78 -0
  147. package/dist/threadline/relay/SybilProtection.d.ts.map +1 -0
  148. package/dist/threadline/relay/SybilProtection.js +187 -0
  149. package/dist/threadline/relay/SybilProtection.js.map +1 -0
  150. package/package.json +9 -3
  151. package/playbook-scripts/build-state.py +529 -0
  152. package/scripts/check-contract-evidence.js +103 -0
  153. package/scripts/pre-push-gate.js +53 -0
  154. package/scripts/run-contract-tests.js +75 -0
  155. package/src/data/builtin-manifest.json +87 -63
  156. package/src/templates/hooks/build-stop-hook.sh +79 -0
  157. package/upgrades/0.26.10.md +36 -0
  158. package/upgrades/0.27.1.md +101 -0
  159. package/upgrades/0.27.2.md +36 -0
  160. package/upgrades/NEXT.md +0 -27
  161. /package/.claude/skills/secret-setup/{SKILL.md → skill.md} +0 -0
@@ -0,0 +1,529 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Build State Manager for Instar /build skill.
4
+
5
+ Manages phase transitions, step tracking, worktree lifecycle, audit logging,
6
+ and state persistence for the rigorous build pipeline.
7
+
8
+ State lives in .instar/state/build/ following Instar's state directory convention.
9
+ Worktree support ensures all /build development happens in isolation.
10
+
11
+ Commands:
12
+ init <task> [--size SMALL|STANDARD|LARGE] Initialize a new build
13
+ transition <phase> [--evidence TEXT] Transition to a new phase
14
+ step-complete <n> <desc> <tests> <passing> Record a completed step
15
+ status Show current build status
16
+ query [--event X] [--phase Y] [--limit N] Query audit log
17
+ report Generate build summary
18
+ complete Mark build as complete
19
+ resume Check if resumable
20
+ worktree-create [--branch NAME] Create isolated worktree
21
+ worktree-merge Merge worktree back to source
22
+ worktree-cleanup Remove worktree
23
+ """
24
+ from __future__ import annotations
25
+
26
+ import argparse
27
+ import json
28
+ import subprocess
29
+ import sys
30
+ from datetime import datetime, timezone
31
+ from pathlib import Path
32
+
33
+ STATE_DIR = Path(".instar/state/build")
34
+ STATE_FILE = STATE_DIR / "build-state.json"
35
+ AUDIT_LOG = STATE_DIR / "audit.jsonl"
36
+ PLAN_FILE = STATE_DIR / "plan.md"
37
+ HISTORY_DIR = STATE_DIR / "history"
38
+
39
+ PHASES = [
40
+ "idle", "clarify", "planning", "executing",
41
+ "verifying", "fixing", "hardening",
42
+ "complete", "failed", "escalated",
43
+ ]
44
+
45
+ TRANSITIONS = {
46
+ "idle": ["clarify", "planning", "executing"],
47
+ "clarify": ["planning"],
48
+ "planning": ["executing"],
49
+ "executing": ["verifying", "complete"],
50
+ "verifying": ["complete", "fixing", "hardening"],
51
+ "fixing": ["verifying", "failed", "escalated"],
52
+ "hardening": ["complete"],
53
+ "complete": [],
54
+ "failed": [],
55
+ "escalated": [],
56
+ }
57
+
58
+ UNIVERSAL_TARGETS = {"failed", "escalated"}
59
+
60
+ PROTECTION_LEVELS = {
61
+ "SMALL": {"reinforcements": 3, "label": "light"},
62
+ "STANDARD": {"reinforcements": 5, "label": "medium"},
63
+ "LARGE": {"reinforcements": 10, "label": "heavy"},
64
+ }
65
+
66
+
67
+ def now_iso():
68
+ return datetime.now(timezone.utc).isoformat()
69
+
70
+
71
+ def load_state():
72
+ if not STATE_FILE.exists():
73
+ return None
74
+ try:
75
+ return json.loads(STATE_FILE.read_text())
76
+ except (json.JSONDecodeError, IOError):
77
+ return None
78
+
79
+
80
+ def save_state(state):
81
+ STATE_DIR.mkdir(parents=True, exist_ok=True)
82
+ STATE_FILE.write_text(json.dumps(state, indent=2))
83
+
84
+
85
+ def append_audit(event, data=None):
86
+ STATE_DIR.mkdir(parents=True, exist_ok=True)
87
+ entry = {"event": event, "timestamp": now_iso()}
88
+ if data:
89
+ entry.update(data)
90
+ with open(AUDIT_LOG, "a") as f:
91
+ f.write(json.dumps(entry) + "\n")
92
+
93
+
94
+ def read_audit():
95
+ if not AUDIT_LOG.exists():
96
+ return []
97
+ entries = []
98
+ for line in AUDIT_LOG.read_text().splitlines():
99
+ if line.strip():
100
+ try:
101
+ entries.append(json.loads(line))
102
+ except json.JSONDecodeError:
103
+ pass
104
+ return entries
105
+
106
+
107
+ def out(obj):
108
+ print(json.dumps(obj))
109
+
110
+
111
+ def git_run(args, cwd=None):
112
+ """Run a git command and return (success, stdout, stderr)."""
113
+ result = subprocess.run(
114
+ ["git"] + args, capture_output=True, text=True, cwd=cwd)
115
+ return result.returncode == 0, result.stdout.strip(), result.stderr.strip()
116
+
117
+
118
+ # ─── Worktree Commands ──────────────────────────────────────────
119
+
120
+ def cmd_worktree_create(args):
121
+ """Create an isolated git worktree for the build."""
122
+ state = load_state()
123
+ if not state:
124
+ out({"error": "No active build. Run 'init' first."})
125
+ sys.exit(1)
126
+
127
+ if state.get("worktree"):
128
+ out({"error": "Worktree already exists",
129
+ "path": state["worktree"]["path"],
130
+ "branch": state["worktree"]["branch"]})
131
+ sys.exit(1)
132
+
133
+ # Determine branch name
134
+ task_slug = state["task"][:40].lower()
135
+ task_slug = "".join(c if c.isalnum() or c == "-" else "-" for c in task_slug)
136
+ task_slug = task_slug.strip("-")
137
+ branch = args.branch or ("build/%s" % task_slug)
138
+
139
+ # Get current branch
140
+ ok, current_branch, _ = git_run(["rev-parse", "--abbrev-ref", "HEAD"])
141
+ if not ok:
142
+ out({"error": "Not in a git repo"})
143
+ sys.exit(1)
144
+
145
+ # Create worktree path
146
+ worktree_path = ".instar/worktrees/%s" % branch.replace("/", "-")
147
+
148
+ # Create the worktree with a new branch
149
+ ok, stdout, stderr = git_run(
150
+ ["worktree", "add", "-b", branch, worktree_path, current_branch])
151
+ if not ok:
152
+ # Branch might exist already, try without -b
153
+ ok, stdout, stderr = git_run(
154
+ ["worktree", "add", worktree_path, branch])
155
+ if not ok:
156
+ out({"error": "Failed to create worktree",
157
+ "stderr": stderr})
158
+ sys.exit(1)
159
+
160
+ state["worktree"] = {
161
+ "path": worktree_path,
162
+ "branch": branch,
163
+ "sourceBranch": current_branch,
164
+ "createdAt": now_iso(),
165
+ }
166
+ save_state(state)
167
+ append_audit("worktree.created", {
168
+ "path": worktree_path, "branch": branch,
169
+ "sourceBranch": current_branch})
170
+
171
+ out({"status": "worktree_created",
172
+ "path": worktree_path, "branch": branch,
173
+ "sourceBranch": current_branch,
174
+ "instruction": "cd %s to work in isolation" % worktree_path})
175
+
176
+
177
+ def cmd_worktree_merge(args):
178
+ """Merge worktree branch back to source."""
179
+ state = load_state()
180
+ if not state or not state.get("worktree"):
181
+ out({"error": "No active worktree"})
182
+ sys.exit(1)
183
+
184
+ wt = state["worktree"]
185
+ source = wt["sourceBranch"]
186
+ branch = wt["branch"]
187
+
188
+ # Merge the build branch into source
189
+ ok, stdout, stderr = git_run(["merge", branch, "--no-ff",
190
+ "-m", "Merge build/%s" % branch])
191
+ if not ok:
192
+ out({"error": "Merge conflict",
193
+ "stderr": stderr,
194
+ "action": "Resolve conflicts manually, then run worktree-cleanup"})
195
+ sys.exit(1)
196
+
197
+ append_audit("worktree.merged", {
198
+ "branch": branch, "into": source})
199
+
200
+ out({"status": "merged", "branch": branch, "into": source})
201
+
202
+
203
+ def cmd_worktree_cleanup(args):
204
+ """Remove the worktree."""
205
+ state = load_state()
206
+ if not state or not state.get("worktree"):
207
+ out({"error": "No active worktree"})
208
+ sys.exit(1)
209
+
210
+ wt = state["worktree"]
211
+ worktree_path = wt["path"]
212
+ branch = wt["branch"]
213
+
214
+ # Remove worktree
215
+ git_run(["worktree", "remove", worktree_path, "--force"])
216
+
217
+ # Optionally delete the branch
218
+ git_run(["branch", "-d", branch])
219
+
220
+ state["worktree"] = None
221
+ save_state(state)
222
+ append_audit("worktree.cleaned", {"path": worktree_path, "branch": branch})
223
+
224
+ out({"status": "cleaned", "path": worktree_path, "branch": branch})
225
+
226
+
227
+ # ─── Core Commands ───────────────────────────────────────────────
228
+
229
+ def cmd_init(args):
230
+ existing = load_state()
231
+ if existing and existing.get("phase") not in ("complete", "failed", "escalated"):
232
+ out({"error": "Active build exists", "phase": existing["phase"],
233
+ "task": existing["task"], "hint": "Use 'complete' or 'resume' first"})
234
+ sys.exit(1)
235
+
236
+ size = (args.size or "STANDARD").upper()
237
+ if size not in PROTECTION_LEVELS:
238
+ out({"error": "Invalid size. Use SMALL, STANDARD, or LARGE"})
239
+ sys.exit(1)
240
+
241
+ state = {
242
+ "task": args.task, "phase": "idle", "size": size,
243
+ "protection": PROTECTION_LEVELS[size],
244
+ "startedAt": now_iso(), "completedAt": None,
245
+ "currentStep": 0, "totalSteps": 0, "steps": [],
246
+ "totalTests": 0, "allPassing": True,
247
+ "fixIterations": 0, "maxFixIterations": 3,
248
+ "verifyIterations": 0, "maxVerifyIterations": 3,
249
+ "reinforcementsUsed": 0,
250
+ "worktree": None,
251
+ }
252
+ save_state(state)
253
+ append_audit("build.initialized", {"task": args.task, "size": size})
254
+ out({"status": "initialized", "task": args.task, "size": size,
255
+ "protection": PROTECTION_LEVELS[size]["label"],
256
+ "reinforcements": PROTECTION_LEVELS[size]["reinforcements"]})
257
+
258
+
259
+ def cmd_transition(args):
260
+ state = load_state()
261
+ if not state:
262
+ out({"error": "No active build. Run 'init' first."})
263
+ sys.exit(1)
264
+
265
+ current = state["phase"]
266
+ target = args.phase
267
+
268
+ if target not in PHASES:
269
+ out({"error": "Invalid phase: %s" % target, "valid": PHASES})
270
+ sys.exit(1)
271
+
272
+ legal = TRANSITIONS.get(current, [])
273
+ if target not in legal and target not in UNIVERSAL_TARGETS:
274
+ out({"error": "Cannot transition %s -> %s" % (current, target),
275
+ "legal_targets": list(set(legal) | UNIVERSAL_TARGETS)})
276
+ sys.exit(1)
277
+
278
+ if target == "fixing":
279
+ state["fixIterations"] = state.get("fixIterations", 0) + 1
280
+ if state["fixIterations"] > state.get("maxFixIterations", 3):
281
+ state["phase"] = "escalated"
282
+ save_state(state)
283
+ append_audit("build.escalated", {
284
+ "reason": "max_fix_iterations",
285
+ "iterations": state["fixIterations"]})
286
+ out({"error": "Max fix iterations exceeded", "action": "escalated"})
287
+ sys.exit(1)
288
+
289
+ if current == "fixing" and target == "verifying":
290
+ state["verifyIterations"] = state.get("verifyIterations", 0) + 1
291
+
292
+ old = state["phase"]
293
+ state["phase"] = target
294
+ save_state(state)
295
+ append_audit("phase.transition", {
296
+ "from": old, "to": target,
297
+ "evidence": args.evidence or None})
298
+ out({"status": "transitioned", "from": old, "to": target})
299
+
300
+
301
+ def cmd_step_complete(args):
302
+ state = load_state()
303
+ if not state:
304
+ out({"error": "No active build."})
305
+ sys.exit(1)
306
+ if state["phase"] != "executing":
307
+ out({"error": "Not in executing phase (currently: %s)" % state["phase"]})
308
+ sys.exit(1)
309
+
310
+ step = {
311
+ "step": args.step_number, "description": args.description,
312
+ "tests": args.tests, "passing": args.passing,
313
+ "timestamp": now_iso(),
314
+ }
315
+ state["steps"].append(step)
316
+ state["currentStep"] = args.step_number
317
+ state["totalTests"] = sum(s["tests"] for s in state["steps"])
318
+ state["allPassing"] = all(s["tests"] == s["passing"] for s in state["steps"])
319
+ save_state(state)
320
+ append_audit("step.completed", {
321
+ "step": args.step_number, "description": args.description,
322
+ "tests": args.tests, "passing": args.passing,
323
+ "totalTests": state["totalTests"]})
324
+ out({"status": "step_completed", "step": args.step_number,
325
+ "tests": args.tests, "passing": args.passing,
326
+ "totalTests": state["totalTests"], "allPassing": state["allPassing"]})
327
+
328
+
329
+ def cmd_status(_args):
330
+ state = load_state()
331
+ if not state:
332
+ out({"status": "no_active_build"})
333
+ return
334
+
335
+ elapsed = None
336
+ if state.get("startedAt"):
337
+ try:
338
+ started = datetime.fromisoformat(
339
+ state["startedAt"].replace("Z", "+00:00"))
340
+ elapsed = (datetime.now(timezone.utc) - started).total_seconds() / 60
341
+ except ValueError:
342
+ pass
343
+
344
+ result = {
345
+ "task": state["task"], "phase": state["phase"],
346
+ "size": state["size"],
347
+ "protection": state.get("protection", {}).get("label", "unknown"),
348
+ "currentStep": state["currentStep"],
349
+ "stepsCompleted": len(state["steps"]),
350
+ "totalTests": state["totalTests"],
351
+ "allPassing": state["allPassing"],
352
+ "fixIterations": state.get("fixIterations", 0),
353
+ "verifyIterations": state.get("verifyIterations", 0),
354
+ "reinforcementsUsed": state.get("reinforcementsUsed", 0),
355
+ "elapsedMinutes": round(elapsed, 1) if elapsed else None,
356
+ }
357
+ if state.get("worktree"):
358
+ result["worktree"] = state["worktree"]
359
+ out(result)
360
+
361
+
362
+ def cmd_query(args):
363
+ entries = read_audit()
364
+ if args.event:
365
+ entries = [e for e in entries if e.get("event") == args.event]
366
+ if args.phase:
367
+ entries = [e for e in entries
368
+ if e.get("to") == args.phase or e.get("from") == args.phase]
369
+ limit = args.limit or 50
370
+ out({"count": len(entries[-limit:]), "entries": entries[-limit:]})
371
+
372
+
373
+ def cmd_report(_args):
374
+ state = load_state()
375
+ if not state:
376
+ print("No active build.")
377
+ return
378
+
379
+ entries = read_audit()
380
+ elapsed_str = "unknown"
381
+ if state.get("startedAt"):
382
+ try:
383
+ started = datetime.fromisoformat(
384
+ state["startedAt"].replace("Z", "+00:00"))
385
+ end = datetime.now(timezone.utc)
386
+ if state.get("completedAt"):
387
+ end = datetime.fromisoformat(
388
+ state["completedAt"].replace("Z", "+00:00"))
389
+ elapsed_str = "%d min" % ((end - started).total_seconds() / 60)
390
+ except ValueError:
391
+ pass
392
+
393
+ event_counts = {}
394
+ for e in entries:
395
+ k = e.get("event", "unknown")
396
+ event_counts[k] = event_counts.get(k, 0) + 1
397
+
398
+ lines = [
399
+ "BUILD REPORT: %s" % state["task"], "",
400
+ "Status: %s" % state["phase"],
401
+ "Size: %s (%s protection)" % (
402
+ state["size"], state.get("protection", {}).get("label", "?")),
403
+ "Duration: %s" % elapsed_str,
404
+ "Steps: %d" % len(state["steps"]),
405
+ "Tests: %d (all passing: %s)" % (state["totalTests"], state["allPassing"]),
406
+ "Fix cycles: %d/%d" % (
407
+ state.get("fixIterations", 0), state.get("maxFixIterations", 3)),
408
+ ]
409
+ if state.get("worktree"):
410
+ lines.append("Worktree: %s (branch: %s)" % (
411
+ state["worktree"]["path"], state["worktree"]["branch"]))
412
+ lines.append("")
413
+ for s in state["steps"]:
414
+ tag = "PASS" if s["tests"] == s["passing"] else "FAIL"
415
+ lines.append(" Step %d: %s [%d tests, %s]" % (
416
+ s["step"], s["description"], s["tests"], tag))
417
+ if event_counts:
418
+ lines.append("")
419
+ lines.append("Audit events:")
420
+ for k, v in sorted(event_counts.items()):
421
+ lines.append(" %s: %d" % (k, v))
422
+ print("\n".join(lines))
423
+
424
+
425
+ def cmd_complete(_args):
426
+ state = load_state()
427
+ if not state:
428
+ out({"error": "No active build."})
429
+ sys.exit(1)
430
+ if not state["allPassing"]:
431
+ out({"error": "Cannot complete: not all tests passing"})
432
+ sys.exit(1)
433
+
434
+ state["phase"] = "complete"
435
+ state["completedAt"] = now_iso()
436
+ save_state(state)
437
+ append_audit("build.complete", {
438
+ "task": state["task"], "totalTests": state["totalTests"],
439
+ "stepsCompleted": len(state["steps"]),
440
+ "fixIterations": state.get("fixIterations", 0)})
441
+
442
+ HISTORY_DIR.mkdir(parents=True, exist_ok=True)
443
+ ts = datetime.now().strftime("%Y%m%d-%H%M%S")
444
+ hist = HISTORY_DIR / ("build-%s.json" % ts)
445
+ hist.write_text(json.dumps(state, indent=2))
446
+
447
+ out({"status": "complete", "task": state["task"],
448
+ "totalTests": state["totalTests"],
449
+ "stepsCompleted": len(state["steps"]),
450
+ "archivedTo": str(hist)})
451
+
452
+
453
+ def cmd_resume(_args):
454
+ state = load_state()
455
+ if not state:
456
+ out({"canResume": False, "reason": "No active build found"})
457
+ return
458
+ if state["phase"] in ("complete", "failed", "escalated"):
459
+ out({"canResume": False,
460
+ "reason": "Terminal state: %s" % state["phase"]})
461
+ return
462
+ result = {
463
+ "canResume": True, "task": state["task"],
464
+ "phase": state["phase"], "size": state["size"],
465
+ "currentStep": state["currentStep"],
466
+ "stepsCompleted": len(state["steps"]),
467
+ "totalTests": state["totalTests"],
468
+ "allPassing": state["allPassing"],
469
+ "planExists": PLAN_FILE.exists(),
470
+ }
471
+ if state.get("worktree"):
472
+ result["worktree"] = state["worktree"]
473
+ out(result)
474
+
475
+
476
+ def main():
477
+ p = argparse.ArgumentParser(description="Instar Build State Manager")
478
+ sub = p.add_subparsers(dest="command")
479
+
480
+ s = sub.add_parser("init")
481
+ s.add_argument("task")
482
+ s.add_argument("--size", choices=["SMALL", "STANDARD", "LARGE"],
483
+ default="STANDARD")
484
+
485
+ s = sub.add_parser("transition")
486
+ s.add_argument("phase")
487
+ s.add_argument("--evidence")
488
+
489
+ s = sub.add_parser("step-complete")
490
+ s.add_argument("step_number", type=int)
491
+ s.add_argument("description")
492
+ s.add_argument("tests", type=int)
493
+ s.add_argument("passing", type=int)
494
+
495
+ sub.add_parser("status")
496
+
497
+ s = sub.add_parser("query")
498
+ s.add_argument("--event")
499
+ s.add_argument("--phase")
500
+ s.add_argument("--limit", type=int, default=50)
501
+
502
+ sub.add_parser("report")
503
+ sub.add_parser("complete")
504
+ sub.add_parser("resume")
505
+
506
+ s = sub.add_parser("worktree-create")
507
+ s.add_argument("--branch")
508
+
509
+ sub.add_parser("worktree-merge")
510
+ sub.add_parser("worktree-cleanup")
511
+
512
+ args = p.parse_args()
513
+ if not args.command:
514
+ p.print_help()
515
+ sys.exit(1)
516
+
517
+ {
518
+ "init": cmd_init, "transition": cmd_transition,
519
+ "step-complete": cmd_step_complete, "status": cmd_status,
520
+ "query": cmd_query, "report": cmd_report,
521
+ "complete": cmd_complete, "resume": cmd_resume,
522
+ "worktree-create": cmd_worktree_create,
523
+ "worktree-merge": cmd_worktree_merge,
524
+ "worktree-cleanup": cmd_worktree_cleanup,
525
+ }[args.command](args)
526
+
527
+
528
+ if __name__ == "__main__":
529
+ main()
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Pre-publish contract evidence check.
4
+ *
5
+ * If any messaging adapter source files have been modified since the last
6
+ * published version, this script requires fresh contract test evidence.
7
+ *
8
+ * This is the FINAL gate before `npm publish`. It catches the exact scenario
9
+ * that burned us: shipping integration code verified only by mocked unit tests.
10
+ *
11
+ * Bypass (emergency only): SKIP_CONTRACT_CHECK=1 npm publish
12
+ */
13
+
14
+ import fs from 'node:fs';
15
+ import path from 'node:path';
16
+ import { execSync } from 'node:child_process';
17
+ import { fileURLToPath } from 'node:url';
18
+
19
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
20
+ const ROOT = path.resolve(__dirname, '..');
21
+
22
+ if (process.env.SKIP_CONTRACT_CHECK === '1') {
23
+ console.log(' ⚠️ SKIP_CONTRACT_CHECK=1 — bypassing contract evidence check');
24
+ console.log(' You are publishing adapter changes WITHOUT verified API testing.\n');
25
+ process.exit(0);
26
+ }
27
+
28
+ const ADAPTER_PATHS = [
29
+ 'src/messaging/slack/',
30
+ 'src/messaging/telegram/',
31
+ 'src/messaging/whatsapp/',
32
+ 'src/messaging/imessage/',
33
+ ];
34
+
35
+ // Check if adapter files changed since last tag
36
+ let adapterChanges = [];
37
+ try {
38
+ // Find files changed since the last npm version tag
39
+ const lastTag = execSync('git describe --tags --abbrev=0 2>/dev/null || echo ""', {
40
+ encoding: 'utf-8',
41
+ cwd: ROOT,
42
+ }).trim();
43
+
44
+ const diffBase = lastTag || 'HEAD~10';
45
+ const changedFiles = execSync(`git diff --name-only ${diffBase}...HEAD 2>/dev/null || echo ""`, {
46
+ encoding: 'utf-8',
47
+ cwd: ROOT,
48
+ }).trim().split('\n').filter(Boolean);
49
+
50
+ adapterChanges = changedFiles.filter(f =>
51
+ ADAPTER_PATHS.some(prefix => f.startsWith(prefix))
52
+ );
53
+ } catch {
54
+ // If git fails, be conservative — check for evidence anyway
55
+ adapterChanges = ['(unable to determine — checking evidence anyway)'];
56
+ }
57
+
58
+ if (adapterChanges.length === 0) {
59
+ // No adapter changes — no contract evidence required
60
+ process.exit(0);
61
+ }
62
+
63
+ console.log(`\n 📋 Adapter files modified (${adapterChanges.length}):`);
64
+ adapterChanges.slice(0, 5).forEach(f => console.log(` • ${f}`));
65
+ if (adapterChanges.length > 5) console.log(` • ...and ${adapterChanges.length - 5} more`);
66
+
67
+ // Check evidence file
68
+ const evidencePath = path.join(ROOT, '.contract-test-evidence.json');
69
+
70
+ if (!fs.existsSync(evidencePath)) {
71
+ console.log('\n ❌ No contract test evidence found.');
72
+ console.log(' You MUST run contract tests before publishing adapter changes:\n');
73
+ console.log(' SLACK_CONTRACT_BOT_TOKEN=xoxb-... npm run test:contract\n');
74
+ console.log(' This verifies your changes work against the REAL API.');
75
+ console.log(' Emergency bypass: SKIP_CONTRACT_CHECK=1 npm publish\n');
76
+ process.exit(1);
77
+ }
78
+
79
+ try {
80
+ const evidence = JSON.parse(fs.readFileSync(evidencePath, 'utf-8'));
81
+ const ageMs = Date.now() - (evidence.timestamp || 0);
82
+ const maxAgeMs = 4 * 60 * 60 * 1000; // 4 hours
83
+
84
+ if (!evidence.passed) {
85
+ console.log('\n ❌ Contract tests FAILED on their last run.');
86
+ console.log(' Fix the failures and re-run: npm run test:contract\n');
87
+ process.exit(1);
88
+ }
89
+
90
+ if (ageMs > maxAgeMs) {
91
+ const hoursAgo = Math.round(ageMs / 3600000);
92
+ console.log(`\n ❌ Contract test evidence is stale (${hoursAgo}h old, max 4h).`);
93
+ console.log(' Re-run: SLACK_CONTRACT_BOT_TOKEN=xoxb-... npm run test:contract\n');
94
+ process.exit(1);
95
+ }
96
+
97
+ const minutesAgo = Math.round(ageMs / 60000);
98
+ console.log(`\n ✅ Contract tests passed ${minutesAgo}m ago — evidence valid\n`);
99
+
100
+ } catch (err) {
101
+ console.log('\n ❌ Contract evidence file is corrupt. Re-run: npm run test:contract\n');
102
+ process.exit(1);
103
+ }
@@ -146,6 +146,59 @@ try {
146
146
  (srcChanges.length > 5 ? `\n • ...and ${srcChanges.length - 5} more` : '')
147
147
  );
148
148
  }
149
+ // ── 4. Adapter contract test gate ─────────────────────────────────────
150
+ // If messaging adapter source files changed, require contract test evidence.
151
+ // This prevents shipping integration code verified only by mocked unit tests.
152
+ // The evidence file is created by `npm run test:contract` when tests pass.
153
+
154
+ const ADAPTER_PATHS = [
155
+ 'src/messaging/slack/',
156
+ 'src/messaging/telegram/',
157
+ 'src/messaging/whatsapp/',
158
+ 'src/messaging/imessage/',
159
+ ];
160
+
161
+ const adapterChanges = srcChanges.filter(f =>
162
+ ADAPTER_PATHS.some(prefix => f.startsWith(prefix))
163
+ );
164
+
165
+ if (adapterChanges.length > 0) {
166
+ const evidencePath = path.join(ROOT, '.contract-test-evidence.json');
167
+ let evidenceValid = false;
168
+
169
+ if (fs.existsSync(evidencePath)) {
170
+ try {
171
+ const evidence = JSON.parse(fs.readFileSync(evidencePath, 'utf-8'));
172
+ const ageMs = Date.now() - (evidence.timestamp || 0);
173
+ const maxAgeMs = 4 * 60 * 60 * 1000; // 4 hours
174
+
175
+ if (ageMs < maxAgeMs && evidence.passed === true) {
176
+ evidenceValid = true;
177
+ const ageMin = Math.round(ageMs / 60000);
178
+ console.log(` ✅ Contract tests passed ${ageMin}m ago (${evidence.suite})`);
179
+ } else if (ageMs >= maxAgeMs) {
180
+ console.log(` ⏰ Contract test evidence is stale (${Math.round(ageMs / 3600000)}h old)`);
181
+ } else if (!evidence.passed) {
182
+ console.log(` ❌ Contract test evidence shows FAILURE`);
183
+ }
184
+ } catch {
185
+ // Corrupt evidence file
186
+ }
187
+ }
188
+
189
+ if (!evidenceValid) {
190
+ errors.push(
191
+ `Adapter source files changed but no recent contract test evidence found.\n` +
192
+ ` Adapter files modified:\n` +
193
+ adapterChanges.slice(0, 5).map(f => ` • ${f}`).join('\n') +
194
+ (adapterChanges.length > 5 ? `\n • ...and ${adapterChanges.length - 5} more` : '') +
195
+ `\n\n Run contract tests against the REAL API before pushing:\n` +
196
+ ` SLACK_CONTRACT_BOT_TOKEN=xoxb-... npm run test:contract\n\n` +
197
+ ` This ensures your changes work against the actual API, not just mocked responses.\n` +
198
+ ` Contract test evidence expires after 4 hours.`
199
+ );
200
+ }
201
+ }
149
202
  } catch {
150
203
  // Git commands may fail in CI or detached HEAD — skip gracefully
151
204
  }