toolroll 0.9.84 → 0.9.86
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/CHANGELOG.md +66 -0
- package/README.md +94 -1193
- package/dist/agent-fence.d.ts +9 -5
- package/dist/agent-fence.js +6 -5
- package/dist/agent-onboard.d.ts +2 -0
- package/dist/agent-onboard.js +3 -0
- package/dist/assignment.js +4 -3
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +33 -31
- package/dist/browser-workspace.d.ts +45 -0
- package/dist/chat-actions.d.ts +11 -1
- package/dist/chat-actions.js +53 -1
- package/dist/chat-channel.d.ts +10 -1
- package/dist/chat-channel.js +36 -5
- package/dist/chat-decide.d.ts +16 -4
- package/dist/chat-decide.js +239 -31
- package/dist/chat-messages.d.ts +5 -0
- package/dist/chat-messages.js +1 -1
- package/dist/chat-quiet.js +6 -4
- package/dist/chat-review.d.ts +4 -0
- package/dist/chat-review.js +13 -4
- package/dist/chat-terms.d.ts +9 -0
- package/dist/chat-terms.js +46 -0
- package/dist/claim.d.ts +3 -0
- package/dist/claim.js +4 -1
- package/dist/contracts/chat-actions.d.ts +9 -0
- package/dist/contracts/chat-actions.js +3 -0
- package/dist/contracts/console-api.d.ts +6 -2
- package/dist/contracts/console-api.js +6 -2
- package/dist/contracts/flow-propose.d.ts +4 -1
- package/dist/contracts/flow-propose.js +1 -1
- package/dist/contracts/flow.d.ts +41 -0
- package/dist/contracts/flow.js +4 -2
- package/dist/contracts/lead-tools.d.ts +51 -0
- package/dist/contracts/lead-tools.js +5 -0
- package/dist/contracts/store-json.d.ts +2 -0
- package/dist/contracts/trigger-payloads.d.ts +20 -0
- package/dist/contracts/trigger-payloads.js +4 -0
- package/dist/contracts/youiyoux-request.d.ts +248 -0
- package/dist/contracts/youiyoux-request.js +152 -0
- package/dist/coordinator.js +1 -0
- package/dist/deploy-gate.js +4 -0
- package/dist/dispatch.d.ts +3 -0
- package/dist/dispatch.js +4 -1
- package/dist/flow-browser.d.ts +103 -0
- package/dist/flow-browser.js +204 -0
- package/dist/flow-cadence.d.ts +8 -2
- package/dist/flow-cadence.js +16 -3
- package/dist/flow-card-task.d.ts +9 -0
- package/dist/flow-card-task.js +15 -6
- package/dist/flow-code.d.ts +5 -1
- package/dist/flow-code.js +4 -1
- package/dist/flow-engine.js +16 -0
- package/dist/flow-live.js +9 -1
- package/dist/flow-pull-request.js +15 -7
- package/dist/flow-share.js +3 -1
- package/dist/flow-steps.d.ts +9 -1
- package/dist/flow-steps.js +55 -5
- package/dist/flow-triggers.js +55 -0
- package/dist/flows-cli.d.ts +19 -0
- package/dist/flows-cli.js +47 -2
- package/dist/flows-ui.js +11 -2
- package/dist/flows.js +5 -2
- package/dist/integrations-ui.js +9 -4
- package/dist/integrations.d.ts +9 -1
- package/dist/integrations.js +43 -2
- package/dist/landed-cleanup.d.ts +4 -0
- package/dist/landed-cleanup.js +3 -2
- package/dist/lead-capabilities.js +2 -1
- package/dist/lead-cli.js +3 -1
- package/dist/lead-tools.d.ts +2 -0
- package/dist/lead-tools.js +65 -10
- package/dist/lead-voice.js +16 -5
- package/dist/lead.d.ts +2 -0
- package/dist/lead.js +3 -3
- package/dist/operate.d.ts +2 -3
- package/dist/operate.js +226 -21
- package/dist/project-activity.d.ts +20 -0
- package/dist/project-activity.js +23 -0
- package/dist/project-cli.d.ts +1 -0
- package/dist/project-cli.js +36 -19
- package/dist/project-delete.js +7 -3
- package/dist/project-knowledge.d.ts +3 -3
- package/dist/project-outcomes.d.ts +119 -0
- package/dist/project-outcomes.js +79 -0
- package/dist/pull-request-flow.js +35 -4
- package/dist/result-actions.d.ts +4 -0
- package/dist/result-actions.js +41 -9
- package/dist/result-follow-ups.js +4 -0
- package/dist/result-review.d.ts +9 -0
- package/dist/result-review.js +45 -5
- package/dist/scope.d.ts +5 -0
- package/dist/scope.js +7 -0
- package/dist/server/chat.js +4 -2
- package/dist/server/console.css +41 -0
- package/dist/server/flows.js +15 -0
- package/dist/server/pages.js +11 -1
- package/dist/server/render-chat.js +3 -2
- package/dist/server/render-pages.d.ts +15 -2
- package/dist/server/render-pages.js +52 -4
- package/dist/server/render-results.d.ts +2 -0
- package/dist/server/render-results.js +20 -7
- package/dist/server/render-tasks.d.ts +9 -0
- package/dist/server/render-tasks.js +32 -11
- package/dist/server/route-table.js +12 -0
- package/dist/server/settings.js +18 -1
- package/dist/server/task-views.js +11 -0
- package/dist/server/tasks.d.ts +10 -0
- package/dist/server/tasks.js +95 -7
- package/dist/settings-proofs.js +1 -0
- package/dist/store.d.ts +116 -5
- package/dist/store.js +385 -24
- package/dist/surface.js +18 -2
- package/dist/task-archive.d.ts +10 -0
- package/dist/task-archive.js +16 -0
- package/dist/task-control.d.ts +1 -1
- package/dist/task-source.d.ts +13 -1
- package/dist/task-source.js +17 -1
- package/dist/telegram-lead.js +7 -5
- package/dist/telegram.js +1 -1
- package/dist/tidy.d.ts +130 -0
- package/dist/tidy.js +503 -0
- package/dist/toolroll-update.js +2 -2
- package/dist/webhooks.d.ts +6 -0
- package/dist/webhooks.js +25 -0
- package/dist/work-index.d.ts +28 -0
- package/dist/work-index.js +82 -28
- package/dist/workspace-ui.d.ts +1 -1
- package/dist/workspace-ui.js +3 -2
- package/dist/youiyoux-intake.d.ts +234 -0
- package/dist/youiyoux-intake.js +908 -0
- package/package.json +11 -8
|
@@ -23,17 +23,18 @@ export declare function knowledgeVersion(store: Store, repo: string, actor: stri
|
|
|
23
23
|
/** Chat reads only an admitted project. Keep the full source in the project UI. */
|
|
24
24
|
export declare function conversationKnowledge(store: Store, repo: string, actor: string, reference?: string, decision?: number): {
|
|
25
25
|
title?: never;
|
|
26
|
+
content?: never;
|
|
26
27
|
revision: number;
|
|
27
28
|
stale: boolean;
|
|
28
29
|
notice: string;
|
|
29
30
|
decision?: never;
|
|
30
|
-
content?: never;
|
|
31
31
|
history?: never;
|
|
32
32
|
instructions?: never;
|
|
33
33
|
references?: never;
|
|
34
34
|
decisions?: never;
|
|
35
35
|
} | {
|
|
36
36
|
title?: never;
|
|
37
|
+
content?: never;
|
|
37
38
|
stale?: never;
|
|
38
39
|
decision: {
|
|
39
40
|
repo: string;
|
|
@@ -50,7 +51,6 @@ export declare function conversationKnowledge(store: Store, repo: string, actor:
|
|
|
50
51
|
id: number;
|
|
51
52
|
};
|
|
52
53
|
notice: string;
|
|
53
|
-
content?: never;
|
|
54
54
|
revision?: never;
|
|
55
55
|
history?: never;
|
|
56
56
|
instructions?: never;
|
|
@@ -69,9 +69,9 @@ export declare function conversationKnowledge(store: Store, repo: string, actor:
|
|
|
69
69
|
decisions?: never;
|
|
70
70
|
} | {
|
|
71
71
|
title?: never;
|
|
72
|
+
content?: never;
|
|
72
73
|
stale?: never;
|
|
73
74
|
decision?: never;
|
|
74
|
-
content?: never;
|
|
75
75
|
revision: number;
|
|
76
76
|
history: {
|
|
77
77
|
revision: number;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { Store } from './store.js';
|
|
2
|
+
import type { WorkSummaryAccess } from './work-summary.js';
|
|
3
|
+
export declare const ACTIVITY_DAYS = 60;
|
|
4
|
+
export declare const ACTIVITY_LIMIT = 500;
|
|
5
|
+
export type CompletionActor = 'human' | 'lead' | 'landed' | 'machine-or-unknown';
|
|
6
|
+
export type ActivityFamily = {
|
|
7
|
+
root: string;
|
|
8
|
+
task: string;
|
|
9
|
+
title: string;
|
|
10
|
+
run: number | null;
|
|
11
|
+
href: string;
|
|
12
|
+
state: 'ready' | 'completed' | 'history-unavailable';
|
|
13
|
+
kind: string;
|
|
14
|
+
finishedAt: string | null;
|
|
15
|
+
completedAt: string | null;
|
|
16
|
+
actor: CompletionActor | null;
|
|
17
|
+
checks: string;
|
|
18
|
+
revisions: number | null;
|
|
19
|
+
elapsedMs: number | null;
|
|
20
|
+
runs: number;
|
|
21
|
+
/** Runs whose amount is known (reported-usage.ts runAmountOf): API dollars or subscription usage. */
|
|
22
|
+
knownCostRuns: number;
|
|
23
|
+
subscriptionRuns: number;
|
|
24
|
+
/** API dollars only; subscription usage is never added to them. */
|
|
25
|
+
recordedUsd: number | null;
|
|
26
|
+
subscriptionReportedUsd: number | null;
|
|
27
|
+
/** Every attempt billed in known API dollars; otherwise the family's dollar total is unknown. */
|
|
28
|
+
totalUsd: number | null;
|
|
29
|
+
};
|
|
30
|
+
export type ProjectOutcomes = {
|
|
31
|
+
asOf: string;
|
|
32
|
+
since: string;
|
|
33
|
+
days: number;
|
|
34
|
+
limit: number;
|
|
35
|
+
truncated: boolean;
|
|
36
|
+
ready: {
|
|
37
|
+
count: number;
|
|
38
|
+
oldestFinishedAgeMs: number | null;
|
|
39
|
+
dated: number;
|
|
40
|
+
};
|
|
41
|
+
completed: {
|
|
42
|
+
count: number;
|
|
43
|
+
actors: Record<CompletionActor, number>;
|
|
44
|
+
scouts: number;
|
|
45
|
+
failedChecks: number;
|
|
46
|
+
};
|
|
47
|
+
finishedToDecision: {
|
|
48
|
+
meanMs: number | null;
|
|
49
|
+
samples: number;
|
|
50
|
+
};
|
|
51
|
+
revisions: {
|
|
52
|
+
count: number;
|
|
53
|
+
perCompletedFamily: number | null;
|
|
54
|
+
samples: number;
|
|
55
|
+
};
|
|
56
|
+
cost: {
|
|
57
|
+
recordedUsd: number | null;
|
|
58
|
+
recordedPerCompletedFamilyUsd: number | null;
|
|
59
|
+
runs: number;
|
|
60
|
+
knownRuns: number;
|
|
61
|
+
subscriptionRuns: number;
|
|
62
|
+
subscriptionReportedUsd: number | null;
|
|
63
|
+
fullyCostedFamilies: number;
|
|
64
|
+
families: number;
|
|
65
|
+
};
|
|
66
|
+
incompleteHistory: number;
|
|
67
|
+
families: ActivityFamily[];
|
|
68
|
+
};
|
|
69
|
+
/** Callers supply their current authenticated ceiling. A path alone grants no access. */
|
|
70
|
+
export declare function projectOutcomes(store: Store, project: string, now: Date, access: WorkSummaryAccess): ProjectOutcomes;
|
|
71
|
+
/** The lead's page of one report: every aggregate unchanged, family rows in pages bounded by count AND by their
|
|
72
|
+
* serialized size, so the reply always fits the tool-result cap (lead.ts, converse.ts LEAD_TOOL_RESULT_CAP_BYTES) and
|
|
73
|
+
* the lead reads further with nextOffset. Long ids and titles are shortened; an over-long link is left out. */
|
|
74
|
+
export declare const ACTIVITY_LEAD_PAGE = 10;
|
|
75
|
+
export declare const ACTIVITY_LEAD_PAGE_BYTES = 12000;
|
|
76
|
+
export declare function projectOutcomesPage(report: ProjectOutcomes, offset?: number): {
|
|
77
|
+
asOf: string;
|
|
78
|
+
since: string;
|
|
79
|
+
days: number;
|
|
80
|
+
limit: number;
|
|
81
|
+
truncated: boolean;
|
|
82
|
+
ready: {
|
|
83
|
+
count: number;
|
|
84
|
+
oldestFinishedAgeMs: number | null;
|
|
85
|
+
dated: number;
|
|
86
|
+
};
|
|
87
|
+
completed: {
|
|
88
|
+
count: number;
|
|
89
|
+
actors: Record<CompletionActor, number>;
|
|
90
|
+
scouts: number;
|
|
91
|
+
failedChecks: number;
|
|
92
|
+
};
|
|
93
|
+
finishedToDecision: {
|
|
94
|
+
meanMs: number | null;
|
|
95
|
+
samples: number;
|
|
96
|
+
};
|
|
97
|
+
revisions: {
|
|
98
|
+
count: number;
|
|
99
|
+
perCompletedFamily: number | null;
|
|
100
|
+
samples: number;
|
|
101
|
+
};
|
|
102
|
+
cost: {
|
|
103
|
+
recordedUsd: number | null;
|
|
104
|
+
recordedPerCompletedFamilyUsd: number | null;
|
|
105
|
+
runs: number;
|
|
106
|
+
knownRuns: number;
|
|
107
|
+
subscriptionRuns: number;
|
|
108
|
+
subscriptionReportedUsd: number | null;
|
|
109
|
+
fullyCostedFamilies: number;
|
|
110
|
+
families: number;
|
|
111
|
+
};
|
|
112
|
+
incompleteHistory: number;
|
|
113
|
+
families: (Omit<ActivityFamily, "href"> & {
|
|
114
|
+
href: string | null;
|
|
115
|
+
})[];
|
|
116
|
+
familiesTotal: number;
|
|
117
|
+
familiesOffset: number;
|
|
118
|
+
nextOffset: number | null;
|
|
119
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/** Read-only Activity. A completion is a saved decision, never a claim of
|
|
2
|
+
* passing checks, deployment, review labor, or improved productivity. */
|
|
3
|
+
import { chatResultHref } from './chat-controls.js';
|
|
4
|
+
import { workOutcomeRows } from './work-index.js';
|
|
5
|
+
import { COMPLETION_BY_PERSON, LANDED_ACTOR } from './result-completion.js';
|
|
6
|
+
export const ACTIVITY_DAYS = 60;
|
|
7
|
+
export const ACTIVITY_LIMIT = 500;
|
|
8
|
+
const text = (value) => value == null ? null : String(value);
|
|
9
|
+
const duration = (from, to) => {
|
|
10
|
+
const delta = from === null ? NaN : Date.parse(to) - Date.parse(from);
|
|
11
|
+
return Number.isFinite(delta) && delta >= 0 ? delta : null;
|
|
12
|
+
};
|
|
13
|
+
const actorOf = (actor, detail, byLead) => actor === LANDED_ACTOR ? 'landed'
|
|
14
|
+
: byLead || actor?.startsWith('lead:') ? 'lead'
|
|
15
|
+
: actor?.startsWith('operator:') && detail === COMPLETION_BY_PERSON ? 'human' : 'machine-or-unknown';
|
|
16
|
+
/** Callers supply their current authenticated ceiling. A path alone grants no access. */
|
|
17
|
+
export function projectOutcomes(store, project, now, access) {
|
|
18
|
+
if (access.repos !== null && !access.repos.includes(project))
|
|
19
|
+
throw new Error('Project activity is outside your access.');
|
|
20
|
+
const asOf = now.toISOString(), since = new Date(now.getTime() - ACTIVITY_DAYS * 86400000).toISOString();
|
|
21
|
+
const rows = workOutcomeRows(store, now, access, project, since, ACTIVITY_LIMIT);
|
|
22
|
+
const families = rows.slice(0, ACTIVITY_LIMIT).map(row => {
|
|
23
|
+
const state = row['code'] === 'complete' ? 'completed' : row['code'] === 'ready-to-check' ? 'ready' : 'history-unavailable';
|
|
24
|
+
const finishedAt = text(row['finished_at']), completedAt = text(row['checked_at']);
|
|
25
|
+
const runs = Number(row['runs']), apiRuns = Number(row['api_runs']), subscriptionRuns = Number(row['subscription_runs']);
|
|
26
|
+
const recordedUsd = apiRuns > 0 && Number.isFinite(Number(row['api_microusd'])) ? Number(row['api_microusd']) / 1_000_000 : null;
|
|
27
|
+
const subscriptionReportedUsd = row['subscription_usd'] != null && Number.isFinite(Number(row['subscription_usd'])) ? Number(row['subscription_usd']) : null;
|
|
28
|
+
const knownCostRuns = apiRuns + subscriptionRuns;
|
|
29
|
+
const task = String(row['task_id']);
|
|
30
|
+
return { root: String(row['root_id']), task, title: String(row['title']), run: row['result_id'] == null ? null : Number(row['result_id']),
|
|
31
|
+
href: row['result_id'] == null ? `/t/${encodeURIComponent(task)}` : chatResultHref(task, Number(row['result_id'])), state, kind: text(row['result_role']) ?? 'unknown',
|
|
32
|
+
finishedAt, completedAt, actor: state === 'completed' ? actorOf(text(row['checked_actor']), text(row['checked_detail']), Number(row['completed_by_lead']) === 1) : null,
|
|
33
|
+
checks: text(row['check_status']) ?? 'not-recorded', revisions: state === 'history-unavailable' ? null : Number(row['version_count']) - 1,
|
|
34
|
+
elapsedMs: duration(finishedAt, completedAt ?? asOf), runs, knownCostRuns, subscriptionRuns, recordedUsd, subscriptionReportedUsd,
|
|
35
|
+
totalUsd: runs > 0 && runs === apiRuns ? recordedUsd : null };
|
|
36
|
+
});
|
|
37
|
+
const ready = families.filter(one => one.state === 'ready'), completed = families.filter(one => one.state === 'completed');
|
|
38
|
+
const times = completed.flatMap(one => one.elapsedMs === null ? [] : [one.elapsedMs]);
|
|
39
|
+
const ages = ready.flatMap(one => one.elapsedMs === null ? [] : [one.elapsedMs]);
|
|
40
|
+
const actors = { human: 0, lead: 0, landed: 0, 'machine-or-unknown': 0 };
|
|
41
|
+
for (const one of completed)
|
|
42
|
+
actors[one.actor]++;
|
|
43
|
+
const knownRuns = completed.reduce((sum, one) => sum + one.knownCostRuns, 0);
|
|
44
|
+
const recordedUsd = completed.some(one => one.recordedUsd !== null) ? completed.reduce((sum, one) => sum + (one.recordedUsd ?? 0), 0) : null;
|
|
45
|
+
const subscriptionRuns = completed.reduce((sum, one) => sum + one.subscriptionRuns, 0);
|
|
46
|
+
const subscriptionReportedUsd = completed.some(one => one.subscriptionReportedUsd !== null) ? completed.reduce((sum, one) => sum + (one.subscriptionReportedUsd ?? 0), 0) : null;
|
|
47
|
+
const revisions = completed.reduce((sum, one) => sum + (one.revisions ?? 0), 0);
|
|
48
|
+
return { asOf, since, days: ACTIVITY_DAYS, limit: ACTIVITY_LIMIT, truncated: rows.length > ACTIVITY_LIMIT,
|
|
49
|
+
ready: { count: ready.length, oldestFinishedAgeMs: ages.length ? Math.max(...ages) : null, dated: ages.length },
|
|
50
|
+
completed: { count: completed.length, actors, scouts: completed.filter(one => one.kind === 'scout').length, failedChecks: completed.filter(one => one.checks === 'failed').length },
|
|
51
|
+
finishedToDecision: { meanMs: times.length ? times.reduce((a, b) => a + b, 0) / times.length : null, samples: times.length },
|
|
52
|
+
revisions: { count: revisions, perCompletedFamily: completed.length ? revisions / completed.length : null, samples: completed.length },
|
|
53
|
+
cost: { recordedUsd, recordedPerCompletedFamilyUsd: completed.length && recordedUsd !== null ? recordedUsd / completed.length : null,
|
|
54
|
+
runs: completed.reduce((sum, one) => sum + one.runs, 0), knownRuns, subscriptionRuns, subscriptionReportedUsd, fullyCostedFamilies: completed.filter(one => one.totalUsd !== null).length, families: completed.length },
|
|
55
|
+
incompleteHistory: families.filter(one => one.state === 'history-unavailable').length, families };
|
|
56
|
+
}
|
|
57
|
+
/** The lead's page of one report: every aggregate unchanged, family rows in pages bounded by count AND by their
|
|
58
|
+
* serialized size, so the reply always fits the tool-result cap (lead.ts, converse.ts LEAD_TOOL_RESULT_CAP_BYTES) and
|
|
59
|
+
* the lead reads further with nextOffset. Long ids and titles are shortened; an over-long link is left out. */
|
|
60
|
+
export const ACTIVITY_LEAD_PAGE = 10;
|
|
61
|
+
export const ACTIVITY_LEAD_PAGE_BYTES = 12_000;
|
|
62
|
+
const shorten = (value, max) => [...value].length > max ? `${[...value].slice(0, max - 1).join('')}…` : value;
|
|
63
|
+
const embeddedBytes = (value) => Buffer.byteLength(JSON.stringify(JSON.stringify(value)), 'utf8');
|
|
64
|
+
export function projectOutcomesPage(report, offset = 0) {
|
|
65
|
+
const start = Math.max(0, Math.min(Number.isSafeInteger(offset) ? offset : 0, report.families.length));
|
|
66
|
+
const page = [];
|
|
67
|
+
const base = { ...report, families: page, familiesTotal: report.families.length, familiesOffset: start, nextOffset: null };
|
|
68
|
+
for (const one of report.families.slice(start, start + ACTIVITY_LEAD_PAGE)) {
|
|
69
|
+
const row = { ...one, root: shorten(one.root, 200), task: shorten(one.task, 200), title: shorten(one.title, 80), href: Buffer.byteLength(one.href) > 512 ? null : one.href };
|
|
70
|
+
page.push(row);
|
|
71
|
+
if (page.length > 1 && embeddedBytes(base) > ACTIVITY_LEAD_PAGE_BYTES) {
|
|
72
|
+
page.pop();
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const next = start + page.length;
|
|
77
|
+
base.nextOffset = next < report.families.length ? next : null;
|
|
78
|
+
return base;
|
|
79
|
+
}
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* read must show the PR open, on the accepted commit, with checks passing; GitHub's head-commit match guards
|
|
15
15
|
* the race; the branch is deleted; the merge commit and who merged are recorded in the ledger.
|
|
16
16
|
*/
|
|
17
|
-
import { createHash } from "node:crypto";
|
|
17
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
18
18
|
import { run as execRun } from "./exec.js";
|
|
19
19
|
import { bodyHashOf, publicationBody, publicationGrantOf } from "./publish.js";
|
|
20
20
|
import { failingChecks, summarizeChecks } from "./pulls.js";
|
|
@@ -23,7 +23,7 @@ import { requestResultChanges } from "./result-actions.js";
|
|
|
23
23
|
import { revisionSourceOf } from "./result-review.js";
|
|
24
24
|
import { authenticateApprover, personProof } from "./scope.js";
|
|
25
25
|
import { BRANCH_PREFIX, headWithin } from "./names.js";
|
|
26
|
-
import { NIGHTLY_CHECK_ONLY, RELEASE_MERGES_ONLY, nightlyTaskRef, releaseTaskRef } from "./store.js";
|
|
26
|
+
import { NIGHTLY_CHECK_ONLY, RELEASE_MERGES_ONLY, nightlyTaskRef, refArchived, releaseTaskRef } from "./store.js";
|
|
27
27
|
import { belongsToRelease } from "./release-authority.js";
|
|
28
28
|
import { autoReleaseSealed } from "./release-auto.js";
|
|
29
29
|
import { fullCheckGate, requestFollowUpChecks } from "./result-follow-ups.js";
|
|
@@ -292,6 +292,9 @@ export async function followPullRequests(store, seen, options) {
|
|
|
292
292
|
continue;
|
|
293
293
|
const pr = publication.prNumber;
|
|
294
294
|
const taskId = taskIdOf(store, publication);
|
|
295
|
+
// A put-away task (v145) gets no automatic revision, merge or ready notice: restoring it resumes following.
|
|
296
|
+
if (refArchived(store, publication.taskRef))
|
|
297
|
+
continue;
|
|
295
298
|
if (one.remoteState === "MERGED") {
|
|
296
299
|
updateFollow(store, publication.id, { merge_commit: one.mergeCommit ?? "", merged_by: "GitHub", merged_at: clock().toISOString() }, clock());
|
|
297
300
|
continue;
|
|
@@ -397,6 +400,8 @@ export async function mergePullRequest(store, input) {
|
|
|
397
400
|
// The nightly check of main (release-once.md, part 3) is a check only: it never merges.
|
|
398
401
|
const mergedRun = store.getRun(input.runId);
|
|
399
402
|
const mergedTask = mergedRun === null ? null : store.refById(mergedRun.taskRef)?.externalId ?? null;
|
|
403
|
+
if (mergedRun !== null && refArchived(store, mergedRun.taskRef))
|
|
404
|
+
return refuse("archived", "This task is put away. Restore it before merging.");
|
|
400
405
|
if (mergedTask !== null && store.handle.prepare("SELECT 1 AS hit FROM nightly_check WHERE task = ?").get(mergedTask) !== undefined)
|
|
401
406
|
return refuse("check-only", "The nightly check of main is only a check; it never merges.");
|
|
402
407
|
// A release's task merges only through `toolroll release` (release-once.md), which proves the live release, reserves
|
|
@@ -470,8 +475,34 @@ export async function mergePullRequest(store, input) {
|
|
|
470
475
|
return fail("moved", "The pull request has commits that weren't part of the completed result, so it won't be merged from here.");
|
|
471
476
|
if (before.checks !== "passing")
|
|
472
477
|
return fail("checks", before.checks === "failing" ? "Checks are failing, so it can't merge." : before.checks === "running" ? "Checks are still running. Merge once they pass." : "No checks have reported yet. Merge once they pass.");
|
|
473
|
-
|
|
474
|
-
|
|
478
|
+
// The merge is reserved with the put-away check in one write (v145): a put away that committed first refuses it, and
|
|
479
|
+
// one that comes after waits for GitHub's answer (the reservation counts as work going on).
|
|
480
|
+
// One merge at a time, each owning its reservation by its token: a second caller is refused, and each releases only its own.
|
|
481
|
+
const token = randomUUID();
|
|
482
|
+
const reserved = store.transact(() => {
|
|
483
|
+
if (refArchived(store, publication.taskRef))
|
|
484
|
+
return "archived";
|
|
485
|
+
if (store.handle.prepare("SELECT 1 FROM hold WHERE task_ref = ? AND owner_id LIKE 'project-delete:%' AND (until IS NULL OR until > ?)").get(publication.taskRef, clock().toISOString()) !== undefined)
|
|
486
|
+
return "deleting";
|
|
487
|
+
if (store.handle.prepare("SELECT 1 FROM merge_in_flight WHERE publication = ? AND at > ?").get(publication.id, new Date(clock().getTime() - 15 * 60_000).toISOString()) !== undefined)
|
|
488
|
+
return "merging";
|
|
489
|
+
store.handle.prepare("INSERT OR REPLACE INTO merge_in_flight (publication, task_ref, token, by, at) VALUES (?, ?, ?, ?, ?)").run(publication.id, publication.taskRef, token, input.by, clock().toISOString());
|
|
490
|
+
return "ok";
|
|
491
|
+
});
|
|
492
|
+
if (reserved === "archived")
|
|
493
|
+
return refuse("archived", "This task is put away. Restore it before merging.");
|
|
494
|
+
if (reserved === "deleting")
|
|
495
|
+
return refuse("deleting", "This task's project is being deleted, so nothing merges.");
|
|
496
|
+
if (reserved === "merging")
|
|
497
|
+
return refuse("merging", "This pull request is already being merged. Wait for that to finish.");
|
|
498
|
+
let merged, after;
|
|
499
|
+
try {
|
|
500
|
+
merged = await exec("gh", ["pr", "merge", pr, "--repo", publication.githubRepo, `--${method}`, "--match-head-commit", publication.headSha, "--delete-branch"], { timeoutMs: EXEC_TIMEOUT_MS, env: { GH_PROMPT_DISABLED: "1", GH_NO_UPDATE_NOTIFIER: "1" } });
|
|
501
|
+
after = await view();
|
|
502
|
+
}
|
|
503
|
+
finally {
|
|
504
|
+
store.handle.prepare("DELETE FROM merge_in_flight WHERE publication = ? AND token = ?").run(publication.id, token);
|
|
505
|
+
}
|
|
475
506
|
if (after?.state === "MERGED") {
|
|
476
507
|
record(after.commit, input.by);
|
|
477
508
|
return { ok: true, commit: after.commit };
|
package/dist/result-actions.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Store } from "./store.js";
|
|
2
|
+
import { type FeedbackImage } from "./result-review.js";
|
|
2
3
|
export type RevisionResult = {
|
|
3
4
|
ok: true;
|
|
4
5
|
id: string;
|
|
@@ -8,6 +9,8 @@ export type RevisionResult = {
|
|
|
8
9
|
message: string;
|
|
9
10
|
};
|
|
10
11
|
type RevisionInput = Parameters<typeof createResultRevision>[2];
|
|
12
|
+
/** Re-prove image custody and current result at each write. No file paths supplied by callers. */
|
|
13
|
+
export declare function feedbackImageProblem(store: Store, root: string, runId: number, image: FeedbackImage | null | undefined): string | null;
|
|
11
14
|
/** One user action: add the typed change and seal exactly the displayed
|
|
12
15
|
* saved notes. A refusal rolls back the note; a replay returns the same task. */
|
|
13
16
|
export declare function requestResultChanges(store: Store, evidenceRoot: string, input: RevisionInput & {
|
|
@@ -15,6 +18,7 @@ export declare function requestResultChanges(store: Store, evidenceRoot: string,
|
|
|
15
18
|
path: string;
|
|
16
19
|
line: string;
|
|
17
20
|
request: string | null;
|
|
21
|
+
image?: FeedbackImage | null;
|
|
18
22
|
}, now: Date): RevisionResult;
|
|
19
23
|
export declare function createResultRevision(store: Store, evidenceRoot: string, input: {
|
|
20
24
|
run: number;
|
package/dist/result-actions.js
CHANGED
|
@@ -1,13 +1,33 @@
|
|
|
1
|
+
import { verifyResultImage } from "./chat-evidence.js";
|
|
1
2
|
/** Shared result-to-revision action for the result form and chat.
|
|
2
3
|
* The caller authenticates; this service checks project access and the exact
|
|
3
4
|
* displayed note batch, then uses the existing atomic revision/approval boundary.
|
|
4
5
|
*/
|
|
5
6
|
import { createHash, randomBytes } from "node:crypto";
|
|
6
7
|
import { EVIDENCE_CAPS, readVerifiedArtifact, writeEvidenceFile } from "./evidence.js";
|
|
7
|
-
import { isRevisionFeedback, parseRevisionBatch, revisionSourceOf, commentSourceKey, REQUEST_TOKEN } from "./result-review.js";
|
|
8
|
+
import { parseFeedbackImage, sameFeedbackImage, isRevisionFeedback, parseRevisionBatch, revisionSourceOf, commentSourceKey, REQUEST_TOKEN } from "./result-review.js";
|
|
8
9
|
import { validateNote, hasForbiddenControls } from "./decision.js";
|
|
9
10
|
import { TASK_TEXT_LIMITS, validateTaskText } from "./task-text.js";
|
|
10
11
|
import { modeFilingCoverage } from "./scope.js";
|
|
12
|
+
/** Re-prove image custody and current result at each write. No file paths supplied by callers. */
|
|
13
|
+
export function feedbackImageProblem(store, root, runId, image) {
|
|
14
|
+
if (image == null)
|
|
15
|
+
return null;
|
|
16
|
+
const parsed = parseFeedbackImage(image);
|
|
17
|
+
if (!parsed || image.run !== runId)
|
|
18
|
+
return "Choose a screenshot from this result. Your draft is unchanged.";
|
|
19
|
+
const run = store.getRun(runId);
|
|
20
|
+
const task = run === null ? null : store.externalIdFor(run.taskRef);
|
|
21
|
+
const repo = run === null ? null : store.refById(run.taskRef)?.repo;
|
|
22
|
+
if (!task || !repo)
|
|
23
|
+
return "This screenshot is no longer available. Your draft is unchanged.";
|
|
24
|
+
const family = store.taskFamilyOf(task, [repo], false);
|
|
25
|
+
const latest = store.runsFor(run.taskRef).find(one => ["builder", "repair", "scout"].includes(one.role) && one.outcome !== null);
|
|
26
|
+
if (family?.problem !== null || family.current.id !== task || latest?.id !== runId)
|
|
27
|
+
return "A newer result is current. Open it before commenting on a screenshot. Your draft is unchanged.";
|
|
28
|
+
const verified = verifyResultImage(store, root, [repo], { taskId: task, ...image });
|
|
29
|
+
return verified.ok ? null : "This screenshot is unavailable or changed. Restore it or remove it from your feedback. Your draft is unchanged.";
|
|
30
|
+
}
|
|
11
31
|
/** One user action: add the typed change and seal exactly the displayed
|
|
12
32
|
* saved notes. A refusal rolls back the note; a replay returns the same task. */
|
|
13
33
|
export function requestResultChanges(store, evidenceRoot, input, now) {
|
|
@@ -15,8 +35,8 @@ export function requestResultChanges(store, evidenceRoot, input, now) {
|
|
|
15
35
|
return store.transact(() => store.savepoint(() => {
|
|
16
36
|
const fail = (status, message) => { throw new ChangeRequestRefusal(status, message); };
|
|
17
37
|
if (!input.note.trim()) {
|
|
18
|
-
if (input.path.trim() || input.line.trim())
|
|
19
|
-
return fail(400, "Describe the change before attaching
|
|
38
|
+
if (input.path.trim() || input.line.trim() || input.image != null)
|
|
39
|
+
return fail(400, "Describe the change before attaching an image, file or line.");
|
|
20
40
|
if (!input.batch)
|
|
21
41
|
return fail(400, "Describe a change or use a saved note.");
|
|
22
42
|
return createResultRevision(store, evidenceRoot, input, now);
|
|
@@ -43,13 +63,19 @@ export function requestResultChanges(store, evidenceRoot, input, now) {
|
|
|
43
63
|
if (!artifact || !readVerifiedArtifact(evidenceRoot, artifact).ok)
|
|
44
64
|
return fail(409, "The saved changes could not be verified. Restore the result before requesting changes.");
|
|
45
65
|
const sourceKey = commentSourceKey(input.actor, input.request);
|
|
46
|
-
|
|
47
|
-
if (
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
66
|
+
const earlier = store.diffCommentBySourceKey(sourceKey);
|
|
67
|
+
if (earlier && (earlier.run !== run.id || earlier.path !== path || earlier.line !== line || earlier.note !== valid.note || !sameFeedbackImage(earlier.image, input.image)))
|
|
68
|
+
return fail(409, "This submission already saved different feedback. Reopen the result and try again.");
|
|
69
|
+
// An exact consumed replay only returns its existing revision. Every new
|
|
70
|
+
// use proves the image before insertion, so malformed targets refuse cleanly.
|
|
71
|
+
if (earlier?.consumedBy == null) {
|
|
72
|
+
const problem = feedbackImageProblem(store, evidenceRoot, run.id, input.image);
|
|
73
|
+
if (problem !== null)
|
|
74
|
+
return fail(409, problem);
|
|
52
75
|
}
|
|
76
|
+
const noteId = earlier?.id ?? store.addDiffComment({ artifactId: artifact.id, runId: run.id, path, line, note: valid.note, author: input.actor, sourceKey, image: input.image ?? null }, now);
|
|
77
|
+
if (noteId === null)
|
|
78
|
+
return fail(409, "This feedback changed while saving. Your draft is unchanged; reopen the result.");
|
|
53
79
|
const result = createResultRevision(store, evidenceRoot, { ...input, batch: [...new Set([...batch, noteId])].sort((a, b) => a - b).join(",") }, now);
|
|
54
80
|
if (!result.ok)
|
|
55
81
|
return fail(result.status, result.message);
|
|
@@ -141,6 +167,11 @@ export function createResultRevision(store, evidenceRoot, input, now) {
|
|
|
141
167
|
}
|
|
142
168
|
if (batch.some(one => !isRevisionFeedback(one)))
|
|
143
169
|
return fail(409, "Review observations are not requested changes. Use Request change to add your feedback first.");
|
|
170
|
+
for (const one of batch) {
|
|
171
|
+
const problem = feedbackImageProblem(store, evidenceRoot, id, one.image);
|
|
172
|
+
if (problem !== null)
|
|
173
|
+
return fail(409, problem);
|
|
174
|
+
}
|
|
144
175
|
const comments = batch;
|
|
145
176
|
// Keep the subject recognizable, including when revising a revision.
|
|
146
177
|
// Reserve the suffix before truncating, at whole grapheme boundaries.
|
|
@@ -180,6 +211,7 @@ export function createResultRevision(store, evidenceRoot, input, now) {
|
|
|
180
211
|
note: one.note,
|
|
181
212
|
author: one.author,
|
|
182
213
|
createdAt: one.createdAt,
|
|
214
|
+
...(one.image == null ? {} : { image: one.image }),
|
|
183
215
|
})),
|
|
184
216
|
};
|
|
185
217
|
const briefBytes = Buffer.from(JSON.stringify(brief, null, 2), "utf8");
|
|
@@ -2,6 +2,7 @@ import { checkoutRunner, ranAtLine, widestRan } from "./invoke.js";
|
|
|
2
2
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
+
import { refArchived } from "./task-archive.js";
|
|
5
6
|
import { belongsToRelease } from "./release-authority.js";
|
|
6
7
|
import { run as execRun } from "./exec.js";
|
|
7
8
|
import { hardenedGit } from "./teardown-git.js";
|
|
@@ -126,6 +127,9 @@ function checkTarget(store, runId, level) {
|
|
|
126
127
|
return refuse("no-commit", "This result has no finished commit to check.");
|
|
127
128
|
if (ref.repo === null)
|
|
128
129
|
return refuse("unplaced", "This result isn't in a project, so there is no check to run.");
|
|
130
|
+
// A put-away task (v145) runs no checks: asked for or already waiting, it is refused until restored.
|
|
131
|
+
if (refArchived(store, ref.id))
|
|
132
|
+
return refuse("archived", "This task is put away. Restore it first.");
|
|
129
133
|
const chosen = checkCommandFor(store, ref.repo, level);
|
|
130
134
|
if (chosen.command === null)
|
|
131
135
|
return refuse("no-check", `${ref.repo} has no approved ${level === "quick" ? "quick or full" : "full"} check. Set one in Settings → Projects → Checks.`);
|
package/dist/result-review.d.ts
CHANGED
|
@@ -19,6 +19,14 @@
|
|
|
19
19
|
* selected tab, and the reading position in this tab's sessionStorage,
|
|
20
20
|
* keyed by the server-named account, the task, and the run. */
|
|
21
21
|
import { type ResultFacts, type PublicationFacts, type DisplayStatus } from "./workspace-ui.js";
|
|
22
|
+
/** Optional context for feedback. The sealed diff remains the revision authority. */
|
|
23
|
+
export type FeedbackImage = {
|
|
24
|
+
run: number;
|
|
25
|
+
artifact: number;
|
|
26
|
+
sha256: string;
|
|
27
|
+
};
|
|
28
|
+
export declare function parseFeedbackImage(value: unknown): FeedbackImage | null | false;
|
|
29
|
+
export declare function sameFeedbackImage(a: FeedbackImage | null | undefined, b: FeedbackImage | null | undefined): boolean;
|
|
22
30
|
/** Add re-read evidence health to the existing result/review projection.
|
|
23
31
|
* This is presentation only: the recorded verdict and acceptance stay intact. */
|
|
24
32
|
export declare function evidenceResultStatusOf(result: ResultFacts, publication: PublicationFacts, evidence: EvidenceHealth): DisplayStatus;
|
|
@@ -49,6 +57,7 @@ export type ResultScreenshot = {
|
|
|
49
57
|
path: string;
|
|
50
58
|
caption: string;
|
|
51
59
|
artifactId: number;
|
|
60
|
+
sha256?: string;
|
|
52
61
|
problem: string | null;
|
|
53
62
|
};
|
|
54
63
|
export type ResultEvidenceInput = {
|
package/dist/result-review.js
CHANGED
|
@@ -19,6 +19,26 @@
|
|
|
19
19
|
* selected tab, and the reading position in this tab's sessionStorage,
|
|
20
20
|
* keyed by the server-named account, the task, and the run. */
|
|
21
21
|
import { resultStatusOf } from "./workspace-ui.js";
|
|
22
|
+
export function parseFeedbackImage(value) {
|
|
23
|
+
if (value === undefined || value === null || value === "")
|
|
24
|
+
return null;
|
|
25
|
+
try {
|
|
26
|
+
const image = typeof value === "string" ? JSON.parse(value) : value;
|
|
27
|
+
if (image === null || typeof image !== "object" || Array.isArray(image)
|
|
28
|
+
|| !Number.isSafeInteger(image.run) || image.run < 1
|
|
29
|
+
|| !Number.isSafeInteger(image.artifact) || image.artifact < 1
|
|
30
|
+
|| typeof image.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(image.sha256)
|
|
31
|
+
|| Object.keys(image).some(key => !["run", "artifact", "sha256"].includes(key)))
|
|
32
|
+
return false;
|
|
33
|
+
return { run: image.run, artifact: image.artifact, sha256: image.sha256 };
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export function sameFeedbackImage(a, b) {
|
|
40
|
+
return (a == null && b == null) || (a != null && b != null && a.run === b.run && a.artifact === b.artifact && a.sha256 === b.sha256);
|
|
41
|
+
}
|
|
22
42
|
/** Add re-read evidence health to the existing result/review projection.
|
|
23
43
|
* This is presentation only: the recorded verdict and acceptance stay intact. */
|
|
24
44
|
export function evidenceResultStatusOf(result, publication, evidence) {
|
|
@@ -155,6 +175,8 @@ export function resultFactsFromHtml(html) {
|
|
|
155
175
|
* page. Matched exactly — never a bare open redirect. */
|
|
156
176
|
export function resultReturnTarget(raw, runId, tasks = []) {
|
|
157
177
|
if (raw !== null && raw !== undefined) {
|
|
178
|
+
if (raw === `/r/${runId}?record=1`)
|
|
179
|
+
return raw;
|
|
158
180
|
const review = /^\/review\?result=[A-Za-z0-9._~%-]{1,200}(?:&run=([0-9]{1,15}))?(?:&tab=(?:summary|changes|checks))?$/.exec(raw);
|
|
159
181
|
if (review !== null && (review[1] === undefined || Number(review[1]) === runId))
|
|
160
182
|
return raw;
|
|
@@ -299,6 +321,22 @@ export const RESULT_REVIEW_SCRIPT = String.raw `
|
|
|
299
321
|
document.addEventListener('click',function(ev){var link=ev.target&&ev.target.closest?ev.target.closest('a[href="#request-changes"]'):null;if(link)openForm();});
|
|
300
322
|
if(form){
|
|
301
323
|
var noteBox=form.querySelector('[name=note]'),pathBox=form.querySelector('[name=path]'),lineBox=form.querySelector('[name=line]'),pin=form.querySelector('details.result-pin');
|
|
324
|
+
var imageBox=form.querySelector('[name=image]'),imagePreview=form.querySelector('[data-feedback-image]');
|
|
325
|
+
function showImage(){
|
|
326
|
+
if(!imageBox||!imagePreview)return;
|
|
327
|
+
imagePreview.hidden=!imageBox.value;
|
|
328
|
+
var img=imagePreview.querySelector('img'),caption=imagePreview.querySelector('figcaption'),selected=null;
|
|
329
|
+
document.querySelectorAll('[data-comment-image]').forEach(function(button){if(button.getAttribute('data-comment-image')===imageBox.value)selected=button;});
|
|
330
|
+
img.hidden=!selected;
|
|
331
|
+
if(selected){var identity=JSON.parse(imageBox.value);img.src='/r/'+identity.run+'/evidence/'+identity.artifact;img.alt=selected.getAttribute('data-image-caption');caption.textContent=img.alt+' · Screenshot '+identity.artifact;}
|
|
332
|
+
else{img.removeAttribute('src');caption.textContent='This screenshot is unavailable. Restore it or remove it from your feedback.';}
|
|
333
|
+
}
|
|
334
|
+
document.addEventListener('click',function(event){
|
|
335
|
+
var button=event.target&&event.target.closest?event.target.closest('[data-comment-image],[data-remove-image]'):null;
|
|
336
|
+
if(!button||!imageBox)return;
|
|
337
|
+
event.preventDefault();imageBox.value=button.getAttribute('data-comment-image')||'';showImage();openForm();save();
|
|
338
|
+
if(noteBox){noteBox.dispatchEvent(new Event('input',{bubbles:true}));noteBox.scrollIntoView({block:'center',behavior:'instant'});noteBox.focus();}
|
|
339
|
+
});
|
|
302
340
|
var limit=document.getElementById('comment-note-limit'),requestBox=form.querySelector('[name=request]');
|
|
303
341
|
function updateActions(){
|
|
304
342
|
var typed=!!(noteBox&¬eBox.value.trim()),batch=form.querySelector('[name=batch]');
|
|
@@ -333,7 +371,7 @@ export const RESULT_REVIEW_SCRIPT = String.raw `
|
|
|
333
371
|
// A fresh request identity: 16 random bytes as hex, the server's own shape.
|
|
334
372
|
function mint(){var bytes=new Uint8Array(16);try{crypto.getRandomValues(bytes);}catch(e){for(var i=0;i<16;i++)bytes[i]=Math.floor(Math.random()*256);}
|
|
335
373
|
var hex='';for(var j=0;j<16;j++)hex+=(bytes[j]<16?'0':'')+bytes[j].toString(16);return hex;}
|
|
336
|
-
function payload(){return JSON.stringify([noteBox?noteBox.value:'',pathBox?pathBox.value:'',lineBox?lineBox.value:'']);}
|
|
374
|
+
function payload(){return JSON.stringify([noteBox?noteBox.value:'',pathBox?pathBox.value:'',lineBox?lineBox.value:'',imageBox?imageBox.value:'']);}
|
|
337
375
|
// What the current token was last submitted with; null = never sent.
|
|
338
376
|
var sent=null,bound=null;
|
|
339
377
|
// The server refused this token for a different note or another result:
|
|
@@ -347,7 +385,7 @@ export const RESULT_REVIEW_SCRIPT = String.raw `
|
|
|
347
385
|
var focusNote=function(){setTimeout(function(){noteBox.focus({preventScroll:true});},0);};
|
|
348
386
|
if(document.readyState==='complete')focusNote();else window.addEventListener('load',focusNote);
|
|
349
387
|
}
|
|
350
|
-
if(saved&&typeof saved.note==='string'&&saved.note.length<=
|
|
388
|
+
if(saved&&typeof saved.note==='string'&&saved.note.length<=(noteBox?noteBox.maxLength:4000)){
|
|
351
389
|
if(noteBox&&!noteBox.value)noteBox.value=saved.note;
|
|
352
390
|
if(saved.note)openForm();
|
|
353
391
|
if(pathBox&&!pathBox.value&&typeof saved.path==='string')pathBox.value=saved.path;
|
|
@@ -355,7 +393,9 @@ export const RESULT_REVIEW_SCRIPT = String.raw `
|
|
|
355
393
|
if(pin&&(pathBox&&pathBox.value||lineBox&&lineBox.value))pin.open=true;
|
|
356
394
|
if(typeof saved.request==='string'&&/^[a-f0-9]{32}$/.test(saved.request)&&requestBox)requestBox.value=saved.request;
|
|
357
395
|
if(typeof saved.sent==='string')sent=saved.sent;
|
|
358
|
-
|
|
396
|
+
if(imageBox&&typeof saved.image==='string'&&saved.image.length<250)imageBox.value=saved.image;
|
|
397
|
+
showImage();
|
|
398
|
+
bound=JSON.stringify([saved.note,saved.path||'',saved.line||'',saved.image||'']);
|
|
359
399
|
if(noteBox)noteBox.dispatchEvent(new Event('input'));
|
|
360
400
|
}
|
|
361
401
|
function save(){
|
|
@@ -365,8 +405,8 @@ export const RESULT_REVIEW_SCRIPT = String.raw `
|
|
|
365
405
|
var current=payload();
|
|
366
406
|
if((bound!==null&¤t!==bound)||(sent!==null&¤t!==sent)){sent=null;if(requestBox)requestBox.value=mint();}
|
|
367
407
|
bound=current;
|
|
368
|
-
if(!note&&!path&&!line){write(draftKey,null);return;}
|
|
369
|
-
write(draftKey,{note:note,path:path,line:line,request:requestBox?requestBox.value:'',sent:sent,at:Date.now()});
|
|
408
|
+
if(!note&&!path&&!line&&!(imageBox&&imageBox.value)){write(draftKey,null);return;}
|
|
409
|
+
write(draftKey,{note:note,path:path,line:line,image:imageBox?imageBox.value:'',request:requestBox?requestBox.value:'',sent:sent,at:Date.now()});
|
|
370
410
|
if(!storage&&limit)limit.textContent='Draft stays on this page only. Browser storage is unavailable.';
|
|
371
411
|
}
|
|
372
412
|
form.addEventListener('input',save);
|
package/dist/scope.d.ts
CHANGED
|
@@ -597,6 +597,11 @@ export type ApproveResult = {
|
|
|
597
597
|
| {
|
|
598
598
|
ok: false;
|
|
599
599
|
reason: "machine-proof";
|
|
600
|
+
}
|
|
601
|
+
/** v145 (task-tidy.md): the task is put away; it is restored before anyone can approve it. */
|
|
602
|
+
| {
|
|
603
|
+
ok: false;
|
|
604
|
+
reason: "archived";
|
|
600
605
|
};
|
|
601
606
|
/**
|
|
602
607
|
* Whether this name-and-token pair is a person the store knows. Shared by
|
package/dist/scope.js
CHANGED
|
@@ -48,6 +48,7 @@ import { z } from "zod";
|
|
|
48
48
|
import {} from "./contracts/acceptance-terms.js";
|
|
49
49
|
import { parseContract } from "./contracts/contract.js";
|
|
50
50
|
import { readAcceptance, readScopeTerms, storedRubricSchema } from "./contracts/scope.js";
|
|
51
|
+
import { taskArchived } from "./task-archive.js";
|
|
51
52
|
import { releaseCiHoldOf, releaseCiHoldWords } from "./release-ci-hold.js";
|
|
52
53
|
import { NO_READINESS, exactModelId, legOf, projectRoute, routeDigestOf, routeFromJson, routeJsonProblem, routeWords, } from "./phase-routing.js";
|
|
53
54
|
/** Same shape as a runner's credential, for the same reasons. */
|
|
@@ -1074,6 +1075,9 @@ export function approve(store, taskId, by, now, sawDigest, token, mutation = {})
|
|
|
1074
1075
|
const scope = store.getScope(taskId);
|
|
1075
1076
|
if (scope === null)
|
|
1076
1077
|
return { ok: false, reason: "no-scope" };
|
|
1078
|
+
// A put-away task (v145) is restored first; sealScopeApproval refuses it too, on every road.
|
|
1079
|
+
if (taskArchived(store, taskId))
|
|
1080
|
+
return { ok: false, reason: "archived" };
|
|
1077
1081
|
// The digest is required, not optional. An operator reads a scope, somebody
|
|
1078
1082
|
// rewrites it, and an approval that did not name what it saw would agree to
|
|
1079
1083
|
// the new one in silence — which is the whole failure this guards.
|
|
@@ -1472,6 +1476,9 @@ export function liveReleaseOf(store, taskId, action, now = new Date()) {
|
|
|
1472
1476
|
return { applies: false };
|
|
1473
1477
|
const { authority: a, carry } = found;
|
|
1474
1478
|
const refuse = (why) => ({ applies: true, ok: false, why, authority: a });
|
|
1479
|
+
// A put-away task (v145, task-tidy.md), or a put-away anchor, deploys, merges and carries nothing: restore it first.
|
|
1480
|
+
if (taskArchived(store, taskId) || taskArchived(store, a.anchorTask))
|
|
1481
|
+
return refuse("the release task is put away");
|
|
1475
1482
|
const newest = newestReleaseGeneration(store, a.repo, a.version, a.pr);
|
|
1476
1483
|
if (newest === null || newest.id !== a.id)
|
|
1477
1484
|
return refuse("a newer approval of this release replaced this one");
|
package/dist/server/chat.js
CHANGED
|
@@ -389,9 +389,11 @@ export function createChatHandlers(runtime) {
|
|
|
389
389
|
const id = proposalId, saved = store.getLeadProposal(id), savedAction = saved?.kind === 'action' ? sharedActionPayload(saved.payload) : null;
|
|
390
390
|
if (saved && savedAction && saved.state !== 'pending' && (sharedConversation !== null || store.getLeadThread(saved.thread)?.approver === principal.name) && principal.repos.includes(savedAction.repo) && store.accountCanAccess(principal.name, savedAction.repo)) {
|
|
391
391
|
const task = typeof saved.outcome?.['taskId'] === 'string' ? saved.outcome['taskId'] : typeof savedAction.request['task'] === 'string' ? savedAction.request['task'] : null;
|
|
392
|
-
|
|
392
|
+
// A flow action leads back to its flow (a page path of our own making, never the proposal's words).
|
|
393
|
+
const flowId = savedAction.operation.startsWith('flow_') && Number.isSafeInteger(savedAction.request['flow']) ? Number(savedAction.request['flow']) : null;
|
|
394
|
+
const destination = task ? taskHref(task) : flowId !== null ? `/flows/${flowId}` : `/settings/${savedAction.operation.startsWith('skill_') ? 'skills' : 'knowledge'}?repo=${encodeURIComponent(savedAction.repo)}`;
|
|
393
395
|
const said = typeof saved.outcome?.['said'] === 'string' ? saved.outcome['said'] : saved.state === 'dismissed' ? 'Action dismissed.' : saved.state === 'expired' ? 'This proposal expired. Ask for a fresh proposal.' : 'This action has no completed outcome yet.';
|
|
394
|
-
return sendScreen(response, 200, screen('Action outcome', html `<h1>${savedAction.title}</h1><p>${said}</p><a class="button-link" href="${destination}">${task ? 'Open task' : 'Open project settings'}</a>`, { chrome: chromeFor(savedAction.repo, 'chat') }));
|
|
396
|
+
return sendScreen(response, 200, screen('Action outcome', html `<h1>${savedAction.title}</h1><p>${said}</p><a class="button-link" href="${destination}">${task ? 'Open task' : flowId !== null ? 'Open flow' : 'Open project settings'}</a>`, { chrome: chromeFor(savedAction.repo, 'chat') }));
|
|
395
397
|
}
|
|
396
398
|
const review = mintSharedActionReview(store, principal, id, evidenceRoot, clock()), action = review.payload;
|
|
397
399
|
const terms = action.terms.map(term => html `<p style="white-space:pre-wrap;overflow-wrap:anywhere">${term}</p>`);
|