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.
- package/README.md +1 -1
- package/dist/commands/init.d.ts +5 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +46 -1
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +228 -36
- package/dist/commands/server.js.map +1 -1
- package/dist/commands/slack-cli.d.ts +6 -0
- package/dist/commands/slack-cli.d.ts.map +1 -1
- package/dist/commands/slack-cli.js +99 -1
- package/dist/commands/slack-cli.js.map +1 -1
- package/dist/core/GitSync.d.ts.map +1 -1
- package/dist/core/GitSync.js +47 -5
- package/dist/core/GitSync.js.map +1 -1
- package/dist/core/PostUpdateMigrator.d.ts +7 -1
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +19 -1
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/types.d.ts +9 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/identity/IdentityManager.d.ts +70 -0
- package/dist/identity/IdentityManager.d.ts.map +1 -0
- package/dist/identity/IdentityManager.js +179 -0
- package/dist/identity/IdentityManager.js.map +1 -0
- package/dist/identity/KeyEncryption.d.ts +40 -0
- package/dist/identity/KeyEncryption.d.ts.map +1 -0
- package/dist/identity/KeyEncryption.js +93 -0
- package/dist/identity/KeyEncryption.js.map +1 -0
- package/dist/identity/KeyRevocation.d.ts +68 -0
- package/dist/identity/KeyRevocation.d.ts.map +1 -0
- package/dist/identity/KeyRevocation.js +171 -0
- package/dist/identity/KeyRevocation.js.map +1 -0
- package/dist/identity/KeyRotation.d.ts +43 -0
- package/dist/identity/KeyRotation.d.ts.map +1 -0
- package/dist/identity/KeyRotation.js +81 -0
- package/dist/identity/KeyRotation.js.map +1 -0
- package/dist/identity/Migration.d.ts +50 -0
- package/dist/identity/Migration.d.ts.map +1 -0
- package/dist/identity/Migration.js +125 -0
- package/dist/identity/Migration.js.map +1 -0
- package/dist/identity/RecoveryPhrase.d.ts +43 -0
- package/dist/identity/RecoveryPhrase.d.ts.map +1 -0
- package/dist/identity/RecoveryPhrase.js +93 -0
- package/dist/identity/RecoveryPhrase.js.map +1 -0
- package/dist/identity/index.d.ts +13 -0
- package/dist/identity/index.d.ts.map +1 -0
- package/dist/identity/index.js +20 -0
- package/dist/identity/index.js.map +1 -0
- package/dist/identity/types.d.ts +101 -0
- package/dist/identity/types.d.ts.map +1 -0
- package/dist/identity/types.js +55 -0
- package/dist/identity/types.js.map +1 -0
- package/dist/knowledge/TreeTriage.d.ts.map +1 -1
- package/dist/knowledge/TreeTriage.js +9 -0
- package/dist/knowledge/TreeTriage.js.map +1 -1
- package/dist/memory/TopicMemory.d.ts +2 -0
- package/dist/memory/TopicMemory.d.ts.map +1 -1
- package/dist/memory/TopicMemory.js +55 -0
- package/dist/memory/TopicMemory.js.map +1 -1
- package/dist/messaging/slack/SlackAdapter.d.ts +15 -4
- package/dist/messaging/slack/SlackAdapter.d.ts.map +1 -1
- package/dist/messaging/slack/SlackAdapter.js +131 -14
- package/dist/messaging/slack/SlackAdapter.js.map +1 -1
- package/dist/messaging/slack/SocketModeClient.d.ts +2 -0
- package/dist/messaging/slack/SocketModeClient.d.ts.map +1 -1
- package/dist/messaging/slack/SocketModeClient.js +31 -1
- package/dist/messaging/slack/SocketModeClient.js.map +1 -1
- package/dist/moltbridge/MoltBridgeClient.d.ts +124 -0
- package/dist/moltbridge/MoltBridgeClient.d.ts.map +1 -0
- package/dist/moltbridge/MoltBridgeClient.js +321 -0
- package/dist/moltbridge/MoltBridgeClient.js.map +1 -0
- package/dist/moltbridge/ProfileCompiler.d.ts +68 -0
- package/dist/moltbridge/ProfileCompiler.d.ts.map +1 -0
- package/dist/moltbridge/ProfileCompiler.js +317 -0
- package/dist/moltbridge/ProfileCompiler.js.map +1 -0
- package/dist/moltbridge/index.d.ts +12 -0
- package/dist/moltbridge/index.d.ts.map +1 -0
- package/dist/moltbridge/index.js +11 -0
- package/dist/moltbridge/index.js.map +1 -0
- package/dist/moltbridge/routes.d.ts +21 -0
- package/dist/moltbridge/routes.d.ts.map +1 -0
- package/dist/moltbridge/routes.js +224 -0
- package/dist/moltbridge/routes.js.map +1 -0
- package/dist/moltbridge/types.d.ts +93 -0
- package/dist/moltbridge/types.d.ts.map +1 -0
- package/dist/moltbridge/types.js +20 -0
- package/dist/moltbridge/types.js.map +1 -0
- package/dist/monitoring/PresenceProxy.d.ts +3 -0
- package/dist/monitoring/PresenceProxy.d.ts.map +1 -1
- package/dist/monitoring/PresenceProxy.js +80 -3
- package/dist/monitoring/PresenceProxy.js.map +1 -1
- package/dist/monitoring/SessionMonitor.d.ts +7 -0
- package/dist/monitoring/SessionMonitor.d.ts.map +1 -1
- package/dist/monitoring/SessionMonitor.js +47 -22
- package/dist/monitoring/SessionMonitor.js.map +1 -1
- package/dist/monitoring/TriageOrchestrator.d.ts.map +1 -1
- package/dist/monitoring/TriageOrchestrator.js +25 -1
- package/dist/monitoring/TriageOrchestrator.js.map +1 -1
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +29 -1
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/server/AgentServer.d.ts +1 -0
- package/dist/server/AgentServer.d.ts.map +1 -1
- package/dist/server/AgentServer.js +1 -0
- package/dist/server/AgentServer.js.map +1 -1
- package/dist/server/routes.d.ts +2 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +21 -0
- package/dist/server/routes.js.map +1 -1
- package/dist/threadline/AuthorizationPolicy.d.ts +110 -0
- package/dist/threadline/AuthorizationPolicy.d.ts.map +1 -0
- package/dist/threadline/AuthorizationPolicy.js +226 -0
- package/dist/threadline/AuthorizationPolicy.js.map +1 -0
- package/dist/threadline/DiscoveryWaterfall.d.ts +73 -0
- package/dist/threadline/DiscoveryWaterfall.d.ts.map +1 -0
- package/dist/threadline/DiscoveryWaterfall.js +113 -0
- package/dist/threadline/DiscoveryWaterfall.js.map +1 -0
- package/dist/threadline/ListenerSessionManager.d.ts.map +1 -1
- package/dist/threadline/ListenerSessionManager.js +2 -0
- package/dist/threadline/ListenerSessionManager.js.map +1 -1
- package/dist/threadline/MessageSecurity.d.ts +49 -0
- package/dist/threadline/MessageSecurity.d.ts.map +1 -0
- package/dist/threadline/MessageSecurity.js +93 -0
- package/dist/threadline/MessageSecurity.js.map +1 -0
- package/dist/threadline/SecureInvitation.d.ts +79 -0
- package/dist/threadline/SecureInvitation.d.ts.map +1 -0
- package/dist/threadline/SecureInvitation.js +164 -0
- package/dist/threadline/SecureInvitation.js.map +1 -0
- package/dist/threadline/TrustAuditLog.d.ts +49 -0
- package/dist/threadline/TrustAuditLog.d.ts.map +1 -0
- package/dist/threadline/TrustAuditLog.js +111 -0
- package/dist/threadline/TrustAuditLog.js.map +1 -0
- package/dist/threadline/TrustEvaluator.d.ts +69 -0
- package/dist/threadline/TrustEvaluator.d.ts.map +1 -0
- package/dist/threadline/TrustEvaluator.js +93 -0
- package/dist/threadline/TrustEvaluator.js.map +1 -0
- package/dist/threadline/UnifiedTrustWiring.d.ts +66 -0
- package/dist/threadline/UnifiedTrustWiring.d.ts.map +1 -0
- package/dist/threadline/UnifiedTrustWiring.js +192 -0
- package/dist/threadline/UnifiedTrustWiring.js.map +1 -0
- package/dist/threadline/client/IdentityManager.d.ts +27 -6
- package/dist/threadline/client/IdentityManager.d.ts.map +1 -1
- package/dist/threadline/client/IdentityManager.js +64 -18
- package/dist/threadline/client/IdentityManager.js.map +1 -1
- package/dist/threadline/relay/SybilProtection.d.ts +78 -0
- package/dist/threadline/relay/SybilProtection.d.ts.map +1 -0
- package/dist/threadline/relay/SybilProtection.js +187 -0
- package/dist/threadline/relay/SybilProtection.js.map +1 -0
- package/package.json +9 -3
- package/playbook-scripts/build-state.py +529 -0
- package/scripts/check-contract-evidence.js +103 -0
- package/scripts/pre-push-gate.js +53 -0
- package/scripts/run-contract-tests.js +75 -0
- package/src/data/builtin-manifest.json +87 -63
- package/src/templates/hooks/build-stop-hook.sh +79 -0
- package/upgrades/0.26.10.md +36 -0
- package/upgrades/0.27.1.md +101 -0
- package/upgrades/0.27.2.md +36 -0
- package/upgrades/NEXT.md +0 -27
- /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
|
+
}
|
package/scripts/pre-push-gate.js
CHANGED
|
@@ -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
|
}
|