@vicfei/dsh-super-agent 0.0.0-stage → 0.5.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 (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +144 -2
  3. package/README.zh-CN.md +146 -0
  4. package/dist/assess.d.ts +13 -0
  5. package/dist/assess.js +64 -0
  6. package/dist/assess.js.map +1 -0
  7. package/dist/cli.d.ts +2 -0
  8. package/dist/cli.js +184 -0
  9. package/dist/cli.js.map +1 -0
  10. package/dist/clip.d.ts +3 -0
  11. package/dist/clip.js +42 -0
  12. package/dist/clip.js.map +1 -0
  13. package/dist/domain.d.ts +67 -0
  14. package/dist/domain.js +6 -0
  15. package/dist/domain.js.map +1 -0
  16. package/dist/engine.d.ts +94 -0
  17. package/dist/engine.js +370 -0
  18. package/dist/engine.js.map +1 -0
  19. package/dist/executor.d.ts +43 -0
  20. package/dist/executor.js +100 -0
  21. package/dist/executor.js.map +1 -0
  22. package/dist/gate.d.ts +34 -0
  23. package/dist/gate.js +66 -0
  24. package/dist/gate.js.map +1 -0
  25. package/dist/github/client.d.ts +63 -0
  26. package/dist/github/client.js +63 -0
  27. package/dist/github/client.js.map +1 -0
  28. package/dist/github/source.d.ts +33 -0
  29. package/dist/github/source.js +110 -0
  30. package/dist/github/source.js.map +1 -0
  31. package/dist/github/state.d.ts +15 -0
  32. package/dist/github/state.js +29 -0
  33. package/dist/github/state.js.map +1 -0
  34. package/dist/index.d.ts +13 -0
  35. package/dist/index.js +16 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/panel.d.ts +8 -0
  38. package/dist/panel.html +128 -0
  39. package/dist/panel.js +54 -0
  40. package/dist/panel.js.map +1 -0
  41. package/dist/runner.d.ts +9 -0
  42. package/dist/runner.js +23 -0
  43. package/dist/runner.js.map +1 -0
  44. package/dist/watch.d.ts +19 -0
  45. package/dist/watch.js +53 -0
  46. package/dist/watch.js.map +1 -0
  47. package/dist/work.d.ts +27 -0
  48. package/dist/work.js +127 -0
  49. package/dist/work.js.map +1 -0
  50. package/docs/demo.gif +0 -0
  51. package/examples/executor.json +8 -0
  52. package/examples/policy.json +14 -0
  53. package/package.json +39 -4
package/dist/clip.js ADDED
@@ -0,0 +1,42 @@
1
+ // Reply discipline — the article §3.3 / prototype docs/004 rule: chat replies are one sentence by
2
+ // default with a hard limit of 100 words. clipReply enforces the hard limit; being concise is the
3
+ // caller's (prompt's) job.
4
+ /** Latin tokens count as words; every CJK char counts as one word. */
5
+ export function wordCount(text) {
6
+ const latin = (text.match(/[A-Za-z0-9'’_-]+/g) ?? []).length;
7
+ const cjk = (text.match(/[\u4e00-\u9fff\u3040-\u30ff\uac00-\ud7af]/g) ?? []).length;
8
+ return latin + cjk;
9
+ }
10
+ const CJK = /[\u4e00-\u9fff\u3040-\u30ff\uac00-\ud7af]/;
11
+ const LATIN_WORD = /[A-Za-z0-9'’_-]/;
12
+ export function clipReply(text, maxWords = 100) {
13
+ const trimmed = text.trim();
14
+ if (wordCount(trimmed) <= maxWords)
15
+ return trimmed;
16
+ let used = 0;
17
+ let out = "";
18
+ let latin = "";
19
+ for (const ch of trimmed) {
20
+ if (used >= maxWords)
21
+ break;
22
+ if (LATIN_WORD.test(ch)) {
23
+ latin += ch;
24
+ continue;
25
+ }
26
+ if (latin) {
27
+ out += latin;
28
+ used++;
29
+ latin = "";
30
+ }
31
+ if (CJK.test(ch)) {
32
+ out += ch;
33
+ used++;
34
+ } // CJK has no spaces: charge per char
35
+ else
36
+ out += ch; // separators/punctuation ride along
37
+ }
38
+ if (latin && used < maxWords)
39
+ out += latin;
40
+ return `${out.trimEnd()} …`;
41
+ }
42
+ //# sourceMappingURL=clip.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clip.js","sourceRoot":"","sources":["../src/clip.ts"],"names":[],"mappings":"AAAA,kGAAkG;AAClG,kGAAkG;AAClG,2BAA2B;AAE3B,sEAAsE;AACtE,MAAM,UAAU,SAAS,CAAC,IAAY;IACpC,MAAM,KAAK,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,mBAAmB,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC;IAC7D,MAAM,GAAG,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,4CAA4C,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC;IACpF,OAAO,KAAK,GAAG,GAAG,CAAC;AACrB,CAAC;AAED,MAAM,GAAG,GAAG,2CAA2C,CAAC;AACxD,MAAM,UAAU,GAAG,iBAAiB,CAAC;AAErC,MAAM,UAAU,SAAS,CAAC,IAAY,EAAE,QAAQ,GAAG,GAAG;IACpD,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;IAC5B,IAAI,SAAS,CAAC,OAAO,CAAC,IAAI,QAAQ;QAAE,OAAO,OAAO,CAAC;IACnD,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,IAAI,GAAG,GAAG,EAAE,CAAC;IACb,IAAI,KAAK,GAAG,EAAE,CAAC;IACf,KAAK,MAAM,EAAE,IAAI,OAAO,EAAE,CAAC;QACzB,IAAI,IAAI,IAAI,QAAQ;YAAE,MAAM;QAC5B,IAAI,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC;YAAC,KAAK,IAAI,EAAE,CAAC;YAAC,SAAS;QAAC,CAAC;QACnD,IAAI,KAAK,EAAE,CAAC;YAAC,GAAG,IAAI,KAAK,CAAC;YAAC,IAAI,EAAE,CAAC;YAAC,KAAK,GAAG,EAAE,CAAC;QAAC,CAAC;QAChD,IAAI,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC;YAAC,GAAG,IAAI,EAAE,CAAC;YAAC,IAAI,EAAE,CAAC;QAAC,CAAC,CAAC,qCAAqC;;YACzE,GAAG,IAAI,EAAE,CAAC,CAAC,oCAAoC;IACtD,CAAC;IACD,IAAI,KAAK,IAAI,IAAI,GAAG,QAAQ;QAAE,GAAG,IAAI,KAAK,CAAC;IAC3C,OAAO,GAAG,GAAG,CAAC,OAAO,EAAE,IAAI,CAAC;AAC9B,CAAC"}
@@ -0,0 +1,67 @@
1
+ /** Who the request came from, as the host sees it. Messages feed the decision; they cannot change permissions. */
2
+ export interface Actor {
3
+ login: string;
4
+ /** GitHub Apps / [bot] logins / app slugs. Bots can produce inferred candidates, never explicit requests. */
5
+ isBot: boolean;
6
+ }
7
+ /** One immutable event with the Source's own metadata. Unit of audit and idempotence. */
8
+ export interface Trigger {
9
+ /** Source-scoped unique id; accepting twice yields one Ticket. */
10
+ id: string;
11
+ source: "github";
12
+ /** Receiving scope — the admission table's row key. */
13
+ scope: Scope;
14
+ /** Triggers of one conversation, as defined by the Source (e.g. `github:owner/repo/issues/42`). */
15
+ threadId: string;
16
+ author: Actor;
17
+ /** Did the message address the agent (@mention, reply-to-agent)? */
18
+ addressing: boolean;
19
+ body: string;
20
+ /** UTC ISO8601. */
21
+ ts: string;
22
+ }
23
+ export type Scope = "issue" | "issue_comment" | "pr" | "pr_comment" | "review_comment" | "ci";
24
+ /** Strength of an admitted Trigger. Explicit works in every accept mode; inferred is mode-gated. */
25
+ export type Strength = "explicit" | "inferred";
26
+ /** Progressive-trust ladder for inferred requests. Start in observe; switch to auto after reviewing records. */
27
+ export type AcceptMode = "off" | "observe" | "auto";
28
+ /**
29
+ * Admission table cell. Defaults per docs/002:
30
+ * listed-not-addressing → "inferred"
31
+ * unlisted-addressing → "ignore"
32
+ * unlisted-not-addressing→ "ignore"
33
+ * Cells are data, not code: weakening a cell does not immediately stop Tickets accepted under it
34
+ * (revalidation-before-resume handles that in the Engine — future milestone).
35
+ */
36
+ export type Cell = "inferred" | "ignore";
37
+ export interface AdmissionRow {
38
+ scope: Scope;
39
+ listedNotAddressing: Cell;
40
+ unlistedAddressing: Cell;
41
+ unlistedNotAddressing: Cell;
42
+ }
43
+ /** Versioned assessment rules + each Source's part. */
44
+ export interface Policy {
45
+ version: number;
46
+ /** Human logins allowed to produce requests. */
47
+ allowedList: string[];
48
+ /** Recognized bots; produce inferred candidates at most. */
49
+ botList: string[];
50
+ acceptMode: AcceptMode;
51
+ /** One row per scope; a missing row means all cells "ignore". */
52
+ table: AdmissionRow[];
53
+ }
54
+ export declare function defaultRow(scope: Scope): AdmissionRow;
55
+ /** The state an accepted Trigger's Ticket reports. */
56
+ export type Report = "progress" | "waiting" | "ready" | "blocked" | "failed" | "dropped";
57
+ /** Eval's output. */
58
+ export type DecisionKind = "accept" | "stop" | "ignore" | "unavailable";
59
+ export interface Decision {
60
+ kind: DecisionKind;
61
+ strength?: Strength;
62
+ /** Why — every recorded fact carries its basis. */
63
+ basis: string;
64
+ /** observe mode: a positive assessment recorded as ignore marked "would accept". */
65
+ wouldAccept?: boolean;
66
+ ts: string;
67
+ }
package/dist/domain.js ADDED
@@ -0,0 +1,6 @@
1
+ // Domain types — adapted from dsh-superagent-prototype docs/001 & docs/002 (MIT, DeepSeek).
2
+ // Flow: Source → Trigger → Gate (host-enforced admission) → [assessment] → Ticket → Executor → Channel.
3
+ export function defaultRow(scope) {
4
+ return { scope, listedNotAddressing: "inferred", unlistedAddressing: "ignore", unlistedNotAddressing: "ignore" };
5
+ }
6
+ //# sourceMappingURL=domain.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"domain.js","sourceRoot":"","sources":["../src/domain.ts"],"names":[],"mappings":"AAAA,4FAA4F;AAC5F,wGAAwG;AAqExG,MAAM,UAAU,UAAU,CAAC,KAAY;IACrC,OAAO,EAAE,KAAK,EAAE,mBAAmB,EAAE,UAAU,EAAE,kBAAkB,EAAE,QAAQ,EAAE,qBAAqB,EAAE,QAAQ,EAAE,CAAC;AACnH,CAAC"}
@@ -0,0 +1,94 @@
1
+ import Database from "better-sqlite3";
2
+ import type { Policy, Strength, Trigger } from "./domain.js";
3
+ import type { GateResult, Resolution } from "./gate.js";
4
+ import type { SourceState } from "./github/state.js";
5
+ export interface IngestItem {
6
+ trigger: Trigger;
7
+ gate: GateResult;
8
+ resolution: Resolution;
9
+ }
10
+ export interface IngestReport {
11
+ fresh: number;
12
+ duplicates: number;
13
+ ticketsOpened: number;
14
+ resumed: number;
15
+ refusals: string[];
16
+ }
17
+ export interface TicketView {
18
+ id: number;
19
+ threadId: string;
20
+ triggerId: string;
21
+ opener: string;
22
+ strength: Strength;
23
+ policyVersion: number;
24
+ state: "open" | "ended";
25
+ /** queued (no assignment) / working / needs-person (failed assignment) / ended. */
26
+ stage: "queued" | "working" | "needs-person" | "ended";
27
+ stoppedBy?: string;
28
+ revokedReason?: string;
29
+ resumedCount: number;
30
+ createdAt: string;
31
+ }
32
+ export interface EffectRow {
33
+ id: number;
34
+ kind: string;
35
+ key: string;
36
+ payload: unknown;
37
+ attempts: number;
38
+ }
39
+ export interface DecisionView {
40
+ id: number;
41
+ kind: "accept" | "ignore" | "unavailable";
42
+ strength?: Strength;
43
+ basis: string;
44
+ wouldAccept: boolean;
45
+ policyVersion: number;
46
+ ts: string;
47
+ threadId: string;
48
+ author: string;
49
+ isBot: boolean;
50
+ scope: string;
51
+ addressing: boolean;
52
+ excerpt: string;
53
+ }
54
+ export declare class Engine {
55
+ readonly db: Database.Database;
56
+ constructor(path: string);
57
+ close(): void;
58
+ /** Store the current Policy if its version differs — open Tickets keep their mandate. */
59
+ setPolicyIfChanged(policy: Policy): boolean;
60
+ /** Panel command acting directly: switch acceptMode, versioned. */
61
+ setAcceptMode(mode: Policy["acceptMode"]): Policy | undefined;
62
+ policy(): Policy | undefined;
63
+ positions(): SourceState;
64
+ /** Comment ids the agent itself posted — the Source must never re-ingest them (self-echo). */
65
+ selfPosted(): Set<string>;
66
+ markSelfPosted(commentId: number): void;
67
+ ingest(batch: IngestItem[], nextPositions?: SourceState): IngestReport;
68
+ /** Revalidation before resume (docs/002): inferred additionally requires auto; an
69
+ * allowed-list-based basis additionally requires the author to still be listed. */
70
+ private revalidateFailReason;
71
+ /** A person's stop withdraws the Mandate and ends the Ticket. */
72
+ stopTicket(id: number, byLogin: string): boolean;
73
+ /** Open tickets claimable by work: never assigned, or last assignment ended cleanly
74
+ * (a resumed ticket is re-claimable; a failed one waits for a person). */
75
+ claimQueued(limit: number): {
76
+ id: number;
77
+ threadId: string;
78
+ body: string;
79
+ author: string;
80
+ }[];
81
+ startAssignment(ticketId: number, worktree: string): number;
82
+ endAssignment(id: number, exitCode: number | null, timedOut: boolean, output: string): void;
83
+ /** A ready report ends the Ticket completed (stopped_by stays NULL ⇒ new work opens a new ticket). */
84
+ completeTicket(id: number): boolean;
85
+ /** Public reply enqueue for adapters acting after commit (work reports, notices). */
86
+ enqueueReply(key: string, threadId: string, body: string): void;
87
+ /** Recent decisions with trigger context — the audit trail (panel + observe records). */
88
+ decisions(limit?: number): DecisionView[];
89
+ tickets(): TicketView[];
90
+ takeDue(now?: Date): EffectRow[];
91
+ completeEffect(id: number): void;
92
+ /** Retryable failures back off exponentially (5s→300s cap); the 5th failure is terminal. */
93
+ failEffect(id: number, error: string, retryable?: boolean): void;
94
+ }
package/dist/engine.js ADDED
@@ -0,0 +1,370 @@
1
+ // Engine — the sole writer of domain aggregates, following dsh-superagent-prototype docs/007:
2
+ // synchronous methods, one transaction per command, external work enqueued as durable effects
3
+ // (outbox) inside the transaction and executed after commit. Domain semantics from docs/001–003:
4
+ // - a Trigger accepted twice yields one Ticket (ingest idempotence)
5
+ // - accept creates a Mandate; a person's stop withdraws it; failed revalidation revokes it
6
+ // - a person's message on their own thread resumes an ended (person-stopped) Ticket
7
+ // - weakening policy does not kill open Tickets; resume revalidates against current policy
8
+ import Database from "better-sqlite3";
9
+ import { mkdirSync } from "node:fs";
10
+ import { dirname } from "node:path";
11
+ import { clipReply } from "./clip.js";
12
+ const SCHEMA = `
13
+ CREATE TABLE IF NOT EXISTS kv (k TEXT PRIMARY KEY, v TEXT NOT NULL);
14
+
15
+ CREATE TABLE IF NOT EXISTS trigger (
16
+ id TEXT PRIMARY KEY,
17
+ source TEXT NOT NULL, scope TEXT NOT NULL, thread_id TEXT NOT NULL,
18
+ author_login TEXT NOT NULL, author_is_bot INTEGER NOT NULL,
19
+ addressing INTEGER NOT NULL, body TEXT NOT NULL, ts TEXT NOT NULL,
20
+ ingested_at TEXT NOT NULL
21
+ );
22
+ CREATE INDEX IF NOT EXISTS idx_trigger_thread ON trigger(thread_id);
23
+
24
+ CREATE TABLE IF NOT EXISTS decision (
25
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
26
+ trigger_id TEXT NOT NULL REFERENCES trigger(id),
27
+ kind TEXT NOT NULL, -- accept | ignore | unavailable
28
+ strength TEXT, basis TEXT NOT NULL, would_accept INTEGER NOT NULL DEFAULT 0,
29
+ policy_version INTEGER NOT NULL, ts TEXT NOT NULL
30
+ );
31
+
32
+ CREATE TABLE IF NOT EXISTS ticket (
33
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
34
+ thread_id TEXT NOT NULL,
35
+ trigger_id TEXT NOT NULL REFERENCES trigger(id),
36
+ opener_login TEXT NOT NULL,
37
+ strength TEXT NOT NULL,
38
+ policy_version INTEGER NOT NULL,
39
+ basis TEXT NOT NULL,
40
+ state TEXT NOT NULL DEFAULT 'open', -- open | ended
41
+ stopped_by TEXT, stopped_at TEXT,
42
+ revoked_reason TEXT,
43
+ resumed_count INTEGER NOT NULL DEFAULT 0,
44
+ created_at TEXT NOT NULL, updated_at TEXT NOT NULL
45
+ );
46
+ CREATE INDEX IF NOT EXISTS idx_ticket_thread ON ticket(thread_id);
47
+
48
+ CREATE TABLE IF NOT EXISTS effect (
49
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
50
+ kind TEXT NOT NULL, key TEXT NOT NULL,
51
+ payload TEXT NOT NULL,
52
+ status TEXT NOT NULL DEFAULT 'pending', -- pending | done | failed
53
+ attempts INTEGER NOT NULL DEFAULT 0,
54
+ next_due_at TEXT,
55
+ last_error TEXT,
56
+ created_at TEXT NOT NULL, updated_at TEXT NOT NULL,
57
+ UNIQUE(kind, key)
58
+ );
59
+ CREATE INDEX IF NOT EXISTS idx_effect_due ON effect(status, next_due_at);
60
+
61
+ CREATE TABLE IF NOT EXISTS assignment (
62
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
63
+ ticket_id INTEGER NOT NULL REFERENCES ticket(id),
64
+ state TEXT NOT NULL DEFAULT 'running', -- running | ended
65
+ worktree TEXT,
66
+ exit_code INTEGER, timed_out INTEGER NOT NULL DEFAULT 0,
67
+ output TEXT,
68
+ started_at TEXT NOT NULL, ended_at TEXT
69
+ );
70
+ CREATE INDEX IF NOT EXISTS idx_assignment_ticket ON assignment(ticket_id);
71
+ `;
72
+ const MAX_EFFECT_ATTEMPTS = 5;
73
+ const BACKOFF_BASE_MS = 5_000;
74
+ const BACKOFF_CAP_MS = 300_000;
75
+ export class Engine {
76
+ db;
77
+ constructor(path) {
78
+ mkdirSync(dirname(path), { recursive: true });
79
+ this.db = new Database(path);
80
+ this.db.pragma("journal_mode = WAL");
81
+ this.db.pragma("foreign_keys = ON");
82
+ this.db.pragma("busy_timeout = 5000"); // panel's quick writes alongside poll
83
+ this.db.exec(SCHEMA);
84
+ }
85
+ close() {
86
+ this.db.close();
87
+ }
88
+ // ── policy (kv) ──────────────────────────────────────────────────────────
89
+ /** Store the current Policy if its version differs — open Tickets keep their mandate. */
90
+ setPolicyIfChanged(policy) {
91
+ const row = this.db.prepare("SELECT v FROM kv WHERE k = 'policy'").get();
92
+ if (row && JSON.parse(row.v).version === policy.version)
93
+ return false;
94
+ this.db
95
+ .prepare("INSERT INTO kv (k, v) VALUES ('policy', ?) ON CONFLICT(k) DO UPDATE SET v = excluded.v")
96
+ .run(JSON.stringify(policy));
97
+ return true;
98
+ }
99
+ /** Panel command acting directly: switch acceptMode, versioned. */
100
+ setAcceptMode(mode) {
101
+ const current = this.policy();
102
+ if (!current)
103
+ return undefined;
104
+ if (current.acceptMode === mode)
105
+ return current;
106
+ const next = { ...current, acceptMode: mode, version: current.version + 1 };
107
+ this.setPolicyIfChanged(next);
108
+ return next;
109
+ }
110
+ policy() {
111
+ const row = this.db.prepare("SELECT v FROM kv WHERE k = 'policy'").get();
112
+ return row ? JSON.parse(row.v) : undefined;
113
+ }
114
+ positions() {
115
+ const row = this.db.prepare("SELECT v FROM kv WHERE k = 'positions'").get();
116
+ const zeros = { issue: 0, issue_comment: 0, pr: 0, review_comment: 0 };
117
+ return { positions: row ? { ...zeros, ...JSON.parse(row.v) } : zeros };
118
+ }
119
+ /** Comment ids the agent itself posted — the Source must never re-ingest them (self-echo). */
120
+ selfPosted() {
121
+ const row = this.db.prepare("SELECT v FROM kv WHERE k = 'self_posted'").get();
122
+ return new Set(row ? JSON.parse(row.v) : []);
123
+ }
124
+ markSelfPosted(commentId) {
125
+ const all = this.selfPosted();
126
+ all.add(`github:issue_comment:${commentId}`);
127
+ this.db
128
+ .prepare("INSERT INTO kv (k, v) VALUES ('self_posted', ?) ON CONFLICT(k) DO UPDATE SET v = excluded.v")
129
+ .run(JSON.stringify([...all]));
130
+ }
131
+ // ── ingest (one transaction: triggers + decisions + tickets + resume + positions) ──
132
+ ingest(batch, nextPositions) {
133
+ const now = new Date().toISOString();
134
+ const report = { fresh: 0, duplicates: 0, ticketsOpened: 0, resumed: 0, refusals: [] };
135
+ const policy = this.policy();
136
+ const policyVersion = policy?.version ?? 0;
137
+ const insertTrigger = this.db.prepare(`INSERT OR IGNORE INTO trigger
138
+ (id, source, scope, thread_id, author_login, author_is_bot, addressing, body, ts, ingested_at)
139
+ VALUES (@id, @source, @scope, @threadId, @login, @isBot, @addressing, @body, @ts, @now)`);
140
+ const insertDecision = this.db.prepare(`INSERT INTO decision
141
+ (trigger_id, kind, strength, basis, would_accept, policy_version, ts)
142
+ VALUES (?, ?, ?, ?, ?, ?, ?)`);
143
+ const insertTicket = this.db.prepare(`INSERT INTO ticket
144
+ (thread_id, trigger_id, opener_login, strength, policy_version, basis, created_at, updated_at)
145
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`);
146
+ const enqueue = this.db.prepare(`INSERT INTO effect (kind, key, payload, status, attempts, next_due_at, created_at, updated_at)
147
+ VALUES (?, ?, ?, 'pending', 0, ?, ?, ?)
148
+ ON CONFLICT(kind, key) DO UPDATE SET payload = excluded.payload, status = 'pending',
149
+ attempts = 0, next_due_at = excluded.next_due_at, last_error = NULL, updated_at = excluded.updated_at`);
150
+ const savePositions = this.db.prepare("INSERT INTO kv (k, v) VALUES ('positions', ?) ON CONFLICT(k) DO UPDATE SET v = excluded.v");
151
+ this.db.transaction(() => {
152
+ for (const item of batch) {
153
+ const t = item.trigger;
154
+ const known = insertTrigger.run({
155
+ id: t.id, source: t.source, scope: t.scope, threadId: t.threadId,
156
+ login: t.author.login, isBot: t.author.isBot ? 1 : 0,
157
+ addressing: t.addressing ? 1 : 0, body: t.body, ts: t.ts, now,
158
+ });
159
+ if (known.changes === 0) {
160
+ report.duplicates++; // accepting one Trigger twice yields one Ticket — and one decision
161
+ continue;
162
+ }
163
+ report.fresh++;
164
+ const kind = item.resolution.action === "accept" ? "accept"
165
+ : item.resolution.action === "shadow" ? "ignore" : "ignore";
166
+ const strength = item.gate.strength ?? null;
167
+ const wouldAccept = item.resolution.action === "shadow" ? 1 : 0;
168
+ const basis = "basis" in item.resolution ? item.resolution.basis : item.gate.basis;
169
+ insertDecision.run(t.id, kind, strength, basis, wouldAccept, policyVersion, now);
170
+ if (item.resolution.action === "accept") {
171
+ // An admitted message from an opener with an ended ticket on this thread goes to that
172
+ // ticket (docs/002: "Ended Tickets keep Mandates; a person's message on their own
173
+ // Threads resumes them"); a revoked mandate takes none of its waiting messages;
174
+ // otherwise this is new work.
175
+ const ended = t.author.isBot ? undefined : this.db
176
+ .prepare("SELECT * FROM ticket WHERE thread_id = ? AND opener_login = ? AND state = 'ended' ORDER BY id DESC LIMIT 1")
177
+ .get(t.threadId, t.author.login);
178
+ if (ended && ended.revoked_reason === null) {
179
+ const failReason = this.revalidateFailReason(ended, policy, t.author.login);
180
+ if (failReason) {
181
+ this.db.prepare("UPDATE ticket SET revoked_reason = ?, updated_at = ? WHERE id = ?").run(failReason, now, ended.id);
182
+ report.refusals.push(`ticket #${ended.id} stays ended — ${failReason}`);
183
+ }
184
+ else {
185
+ this.db
186
+ .prepare("UPDATE ticket SET state = 'open', resumed_count = resumed_count + 1, updated_at = ? WHERE id = ?")
187
+ .run(now, ended.id);
188
+ report.resumed++;
189
+ enqueue.run("reply", `ticket-resume:${ended.id}:${Number(ended.resumed_count) + 1}`, JSON.stringify(replyPayload(t, Number(ended.id), "resumed", ended.strength)), now, now, now);
190
+ }
191
+ }
192
+ else if (ended && ended.revoked_reason !== null) {
193
+ report.refusals.push(`ticket #${ended.id} stays ended — mandate revoked`);
194
+ }
195
+ else {
196
+ const info = insertTicket.run(t.threadId, t.id, t.author.login, strength, policyVersion, basis, now, now);
197
+ const ticketId = Number(info.lastInsertRowid);
198
+ report.ticketsOpened++;
199
+ enqueue.run("reply", `ticket-open:${ticketId}`, JSON.stringify(replyPayload(t, ticketId, "opened", strength)), now, now, now);
200
+ }
201
+ }
202
+ }
203
+ if (nextPositions)
204
+ savePositions.run(JSON.stringify(nextPositions.positions));
205
+ })();
206
+ return report;
207
+ }
208
+ /** Revalidation before resume (docs/002): inferred additionally requires auto; an
209
+ * allowed-list-based basis additionally requires the author to still be listed. */
210
+ revalidateFailReason(ticket, policy, authorLogin) {
211
+ if (ticket.strength === "inferred" && policy?.acceptMode !== "auto") {
212
+ return "revalidation failed: inferred mandate requires acceptMode=auto";
213
+ }
214
+ if (String(ticket.basis).includes("allowed")) {
215
+ const allowed = (policy?.allowedList ?? []).map((s) => s.toLowerCase());
216
+ if (!allowed.includes(authorLogin.toLowerCase())) {
217
+ return `revalidation failed: ${authorLogin} no longer allowed`;
218
+ }
219
+ }
220
+ return undefined;
221
+ }
222
+ // ── person commands (panel command acting directly — docs/002) ───────────
223
+ /** A person's stop withdraws the Mandate and ends the Ticket. */
224
+ stopTicket(id, byLogin) {
225
+ const now = new Date().toISOString();
226
+ const r = this.db
227
+ .prepare("UPDATE ticket SET state = 'ended', stopped_by = ?, stopped_at = ?, updated_at = ? WHERE id = ? AND state = 'open'")
228
+ .run(byLogin, now, now, id);
229
+ return r.changes > 0;
230
+ }
231
+ // ── assignments (docs/001: the binding of a Ticket to one Executor instance) ──
232
+ /** Open tickets claimable by work: never assigned, or last assignment ended cleanly
233
+ * (a resumed ticket is re-claimable; a failed one waits for a person). */
234
+ claimQueued(limit) {
235
+ const rows = this.db.prepare(`
236
+ SELECT t.id, t.thread_id, tr.body, tr.author_login
237
+ FROM ticket t JOIN trigger tr ON tr.id = t.trigger_id
238
+ LEFT JOIN assignment la ON la.ticket_id = t.id
239
+ AND la.id = (SELECT MAX(b.id) FROM assignment b WHERE b.ticket_id = t.id)
240
+ WHERE t.state = 'open' AND (la.id IS NULL OR (la.state = 'ended' AND la.exit_code = 0))
241
+ ORDER BY t.id LIMIT ?`).all(limit);
242
+ return rows.map((r) => ({
243
+ id: Number(r.id), threadId: String(r.thread_id),
244
+ body: String(r.body), author: String(r.author_login),
245
+ }));
246
+ }
247
+ startAssignment(ticketId, worktree) {
248
+ const r = this.db
249
+ .prepare("INSERT INTO assignment (ticket_id, worktree, started_at) VALUES (?, ?, ?)")
250
+ .run(ticketId, worktree, new Date().toISOString());
251
+ return Number(r.lastInsertRowid);
252
+ }
253
+ endAssignment(id, exitCode, timedOut, output) {
254
+ this.db
255
+ .prepare("UPDATE assignment SET state = 'ended', exit_code = ?, timed_out = ?, output = ?, ended_at = ? WHERE id = ?")
256
+ .run(exitCode, timedOut ? 1 : 0, output.slice(0, 65536), new Date().toISOString(), id);
257
+ }
258
+ /** A ready report ends the Ticket completed (stopped_by stays NULL ⇒ new work opens a new ticket). */
259
+ completeTicket(id) {
260
+ const r = this.db
261
+ .prepare("UPDATE ticket SET state = 'ended', updated_at = ? WHERE id = ? AND state = 'open'")
262
+ .run(new Date().toISOString(), id);
263
+ return r.changes > 0;
264
+ }
265
+ /** Public reply enqueue for adapters acting after commit (work reports, notices). */
266
+ enqueueReply(key, threadId, body) {
267
+ const now = new Date().toISOString();
268
+ const n = threadId.match(/\/(issues|pulls)\/(\d+)$/);
269
+ this.db.prepare(`INSERT INTO effect (kind, key, payload, status, attempts, next_due_at, created_at, updated_at)
270
+ VALUES ('reply', ?, ?, 'pending', 0, ?, ?, ?)
271
+ ON CONFLICT(kind, key) DO UPDATE SET payload = excluded.payload, status = 'pending',
272
+ attempts = 0, next_due_at = excluded.next_due_at, last_error = NULL, updated_at = excluded.updated_at`)
273
+ .run(key, JSON.stringify({ threadId, issueNumber: n ? Number(n[2]) : null, body }), now, now, now);
274
+ }
275
+ // ── views ────────────────────────────────────────────────────────────────
276
+ /** Recent decisions with trigger context — the audit trail (panel + observe records). */
277
+ decisions(limit = 50) {
278
+ const rows = this.db.prepare(`
279
+ SELECT d.id, d.kind, d.strength, d.basis, d.would_accept, d.policy_version, d.ts,
280
+ t.thread_id, t.author_login, t.author_is_bot, t.scope, t.addressing,
281
+ substr(t.body, 1, 160) AS excerpt
282
+ FROM decision d JOIN trigger t ON t.id = d.trigger_id
283
+ ORDER BY d.id DESC LIMIT ?`).all(limit);
284
+ return rows.map((r) => ({
285
+ id: Number(r.id),
286
+ kind: String(r.kind),
287
+ strength: r.strength ?? undefined,
288
+ basis: String(r.basis),
289
+ wouldAccept: r.would_accept === 1,
290
+ policyVersion: Number(r.policy_version),
291
+ ts: String(r.ts),
292
+ threadId: String(r.thread_id),
293
+ author: String(r.author_login),
294
+ isBot: r.author_is_bot === 1,
295
+ scope: String(r.scope),
296
+ addressing: r.addressing === 1,
297
+ excerpt: String(r.excerpt ?? ""),
298
+ }));
299
+ }
300
+ tickets() {
301
+ const rows = this.db.prepare("SELECT * FROM ticket ORDER BY id DESC").all();
302
+ const lastAssignment = new Map();
303
+ for (const a of this.db.prepare("SELECT ticket_id, state, exit_code FROM assignment ORDER BY id").all()) {
304
+ lastAssignment.set(Number(a.ticket_id), { state: String(a.state), exitCode: a.exit_code === null ? null : Number(a.exit_code) });
305
+ }
306
+ return rows.map((r) => {
307
+ const id = Number(r.id);
308
+ const state = r.state;
309
+ const a = lastAssignment.get(id);
310
+ const stage = state === "ended"
311
+ ? "ended"
312
+ : !a ? "queued"
313
+ : a.state === "running" ? "working"
314
+ : a.exitCode === 0 ? "queued" // ended cleanly but ticket reopened (resume) — re-claimable
315
+ : "needs-person"; // failed assignment awaits a person (docs/003 person stage)
316
+ return {
317
+ id,
318
+ threadId: String(r.thread_id),
319
+ triggerId: String(r.trigger_id),
320
+ opener: String(r.opener_login),
321
+ strength: r.strength,
322
+ policyVersion: Number(r.policy_version),
323
+ state,
324
+ stage,
325
+ ...(r.stopped_by !== null ? { stoppedBy: String(r.stopped_by) } : {}),
326
+ ...(r.revoked_reason !== null ? { revokedReason: String(r.revoked_reason) } : {}),
327
+ resumedCount: Number(r.resumed_count),
328
+ createdAt: String(r.created_at),
329
+ };
330
+ });
331
+ }
332
+ // ── effects (outbox) ─────────────────────────────────────────────────────
333
+ takeDue(now = new Date()) {
334
+ const rows = this.db
335
+ .prepare("SELECT * FROM effect WHERE status = 'pending' AND (next_due_at IS NULL OR next_due_at <= ?) ORDER BY id")
336
+ .all(now.toISOString());
337
+ return rows.map((r) => ({
338
+ id: Number(r.id), kind: String(r.kind), key: String(r.key),
339
+ payload: JSON.parse(String(r.payload)), attempts: Number(r.attempts),
340
+ }));
341
+ }
342
+ completeEffect(id) {
343
+ this.db.prepare("UPDATE effect SET status = 'done', updated_at = ? WHERE id = ?").run(new Date().toISOString(), id);
344
+ }
345
+ /** Retryable failures back off exponentially (5s→300s cap); the 5th failure is terminal. */
346
+ failEffect(id, error, retryable = true) {
347
+ const now = new Date();
348
+ const row = this.db.prepare("SELECT attempts FROM effect WHERE id = ?").get(id);
349
+ if (!row)
350
+ return;
351
+ const attempts = row.attempts + 1;
352
+ if (!retryable || attempts >= MAX_EFFECT_ATTEMPTS) {
353
+ this.db.prepare("UPDATE effect SET status = 'failed', attempts = ?, last_error = ?, updated_at = ? WHERE id = ?")
354
+ .run(attempts, error, now.toISOString(), id);
355
+ return;
356
+ }
357
+ const delay = Math.min(BACKOFF_BASE_MS * 2 ** attempts, BACKOFF_CAP_MS);
358
+ this.db.prepare("UPDATE effect SET attempts = ?, next_due_at = ?, last_error = ?, updated_at = ? WHERE id = ?")
359
+ .run(attempts, new Date(now.getTime() + delay).toISOString(), error, now.toISOString(), id);
360
+ }
361
+ }
362
+ function replyPayload(t, ticketId, verb, strength) {
363
+ const n = t.threadId.match(/\/(issues|pulls)\/(\d+)$/);
364
+ return {
365
+ threadId: t.threadId,
366
+ issueNumber: n ? Number(n[2]) : null,
367
+ body: clipReply(`Ticket #${ticketId} ${verb} (${strength}). This is by an automated agent, not a human.`),
368
+ };
369
+ }
370
+ //# sourceMappingURL=engine.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine.js","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,43 @@
1
+ export interface ExecutorConfig {
2
+ /** Shell command (sh -c). Env: DSH_DIR, DSH_PROMPT_FILE, DSH_TICKET, DSH_THREAD, DSH_REPO, DSH_AUTHOR. */
3
+ command: string;
4
+ /** Local clone to prepare worktrees from. */
5
+ repoDir: string;
6
+ /** Worktrees live here (suggested .state/work). */
7
+ workRoot: string;
8
+ /** Prepare command template with {dir} and {branch}; default uses git worktree add. */
9
+ prepare?: string;
10
+ /** Background cap (docs/003: at most 30 minutes). */
11
+ timeoutMs: number;
12
+ /** Worker slots per run. */
13
+ maxWorkers: number;
14
+ /** Substrings that refuse execution — "fail when invoked by name". */
15
+ blockedCommands: string[];
16
+ }
17
+ export interface ExecutorResult {
18
+ exitCode: number | null;
19
+ timedOut: boolean;
20
+ output: string;
21
+ worktree: string;
22
+ /** Commit the worktree branch was created from — the base for PR delivery diffing. */
23
+ baseSha: string;
24
+ }
25
+ export declare class Executor {
26
+ readonly cfg: ExecutorConfig;
27
+ constructor(cfg: ExecutorConfig);
28
+ /** Throws on blocked command, prepare failure, or unusable config — the caller records failure. */
29
+ run(ticket: {
30
+ id: number;
31
+ threadId: string;
32
+ }, trigger: {
33
+ body: string;
34
+ author: string;
35
+ }, repo: string): Promise<ExecutorResult>;
36
+ }
37
+ export declare function renderPrompt(ticket: {
38
+ id: number;
39
+ threadId: string;
40
+ }, trigger: {
41
+ body: string;
42
+ author: string;
43
+ }, repo: string): string;