arkgate 4.6.5 → 4.6.6
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 +44 -2106
- package/README.md +10 -9
- package/bin/ark-check-runtime.mjs +38 -2
- package/bin/ark.mjs +13 -3
- package/bin/lib/adoption-stance.mjs +104 -0
- package/bin/lib/ci-merge-boundary.mjs +4 -2
- package/bin/lib/design-delta.mjs +2 -2
- package/bin/lib/design-smells.mjs +1 -1
- package/bin/lib/diagnostic-catalog.mjs +1 -1
- package/bin/lib/doctor-advisories.mjs +2 -2
- package/bin/lib/doctor-next-actions.mjs +20 -2
- package/bin/lib/doctor-plan.mjs +152 -135
- package/bin/lib/enforcement-honesty.mjs +70 -0
- package/bin/lib/first-run-help.mjs +8 -7
- package/bin/lib/html-report-advisories.mjs +10 -2
- package/bin/lib/html-report.mjs +2 -2
- package/bin/lib/mcp-adoption.mjs +19 -0
- package/bin/lib/policy-delta-io.mjs +1 -1
- package/bin/lib/post-green-path.mjs +5 -1
- package/bin/lib/product-copy.mjs +6 -3
- package/bin/lib/start-preview.mjs +12 -22
- package/bin/lib/status-command.mjs +16 -0
- package/bin/lib/status-manifest.mjs +8 -2
- package/bin/lib/team-parliament-io.mjs +62 -2
- package/bin/lib/team-parliament.mjs +25 -5
- package/bin/lib/unavailable-analysis.mjs +1 -0
- package/dist/index.cjs +2 -2
- package/dist/index.d.ts +10 -2
- package/dist/index.js +2 -2
- package/docs/README.md +6 -10
- package/docs/ai-gates.md +12 -5
- package/docs/configuration.md +9 -1
- package/docs/diagnostics.md +2 -2
- package/docs/package-surface.md +5 -4
- package/docs/product-voice.md +6 -4
- package/docs/threat-model.md +2 -2
- package/docs/use.md +5 -4
- package/package.json +1 -1
- package/schemas/ark.design-delta.schema.json +1 -1
- package/server.json +2 -2
- package/templates/agent-skills/README.md +1 -1
package/bin/lib/html-report.mjs
CHANGED
|
@@ -110,7 +110,7 @@ export function baselineSignalHint(signal) {
|
|
|
110
110
|
export function modeBadgeHint(mode) {
|
|
111
111
|
switch (String(mode || '').toLowerCase()) {
|
|
112
112
|
case 'enforce':
|
|
113
|
-
return 'Contract matches the tree
|
|
113
|
+
return 'Contract matches the tree on checked import edges. Required GitHub status (or an advisory-only ack) is what adopts the merge boundary.';
|
|
114
114
|
case 'adapt':
|
|
115
115
|
return 'Contract is live but still aligning (optional cores with files, empty cores, or presentation-bag false green).';
|
|
116
116
|
case 'suggest':
|
|
@@ -255,7 +255,7 @@ export function computeReportFitness({ coverage, violations, ok, enforcement, co
|
|
|
255
255
|
const modeBlurb = {
|
|
256
256
|
suggest: 'Starter shape — expand layers as the codebase grows.',
|
|
257
257
|
adapt: 'Contract is live; raise governed coverage or match real folders.',
|
|
258
|
-
enforce: 'Contract
|
|
258
|
+
enforce: 'Contract matches the tree on checked import edges. Merge is adopted only with a required GitHub status or an advisory-only ack.',
|
|
259
259
|
}[mode];
|
|
260
260
|
const scoreCoverage = governedPercent == null ? 50 : governedPercent;
|
|
261
261
|
const scoreClean =
|
package/bin/lib/mcp-adoption.mjs
CHANGED
|
@@ -17,6 +17,11 @@ import { detectActiveAgentHost, skillTemplateNames } from './skill-install.mjs';
|
|
|
17
17
|
import { detectDeployPathQuality } from './deploy-path.mjs';
|
|
18
18
|
import { collectWeakestLinkGaps } from './weakest-link.mjs';
|
|
19
19
|
import { codexRuntimeActivation, withCiProviderEvidence } from './enforcement-state.mjs';
|
|
20
|
+
import {
|
|
21
|
+
classifyAdopted,
|
|
22
|
+
readAdoptionStance,
|
|
23
|
+
NOT_ADOPTED_NEXT_ACTION,
|
|
24
|
+
} from './adoption-stance.mjs';
|
|
20
25
|
|
|
21
26
|
export { detectDeployPathQuality };
|
|
22
27
|
|
|
@@ -528,6 +533,20 @@ export function collectAdoptionGaps(root, config, coverage) {
|
|
|
528
533
|
gaps.push(g);
|
|
529
534
|
}
|
|
530
535
|
|
|
536
|
+
const adoptedKind = classifyAdopted({
|
|
537
|
+
stance: readAdoptionStance(root),
|
|
538
|
+
github: weakest.github,
|
|
539
|
+
});
|
|
540
|
+
if (adoptedKind === 'not-adopted') {
|
|
541
|
+
gaps.push({
|
|
542
|
+
id: 'adoption-stance-missing',
|
|
543
|
+
severity: 'warn',
|
|
544
|
+
message:
|
|
545
|
+
'Merge boundary not adopted: require a GitHub status on arkgate-check --strict-merge, or write .ark/adoption-stance.json with stance: "advisory-only".',
|
|
546
|
+
fix: NOT_ADOPTED_NEXT_ACTION,
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
|
|
531
550
|
return {
|
|
532
551
|
gaps,
|
|
533
552
|
hosts,
|
|
@@ -42,7 +42,7 @@ function repositoryRoot(root) {
|
|
|
42
42
|
return result.status === 0 ? result.stdout.trim() : null;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
function discoverLocalBaseRef(root) {
|
|
45
|
+
export function discoverLocalBaseRef(root) {
|
|
46
46
|
const top = repositoryRoot(root);
|
|
47
47
|
if (!top) return null;
|
|
48
48
|
const remoteHead = runGit(top, ['symbolic-ref', '--short', 'refs/remotes/origin/HEAD']);
|
|
@@ -92,10 +92,14 @@ export function mergePostGreenTopActions(actions, postGreen) {
|
|
|
92
92
|
|
|
93
93
|
/**
|
|
94
94
|
* Whether doctor may print “Healthy — nothing to do”.
|
|
95
|
+
* Empty actions + !designWeak is not Healthy unless the merge boundary is
|
|
96
|
+
* required-merge (advisory-only ack is adopted but not this Healthy string).
|
|
95
97
|
* @param {{ designWeak?: boolean } | null | undefined} designFitness
|
|
96
98
|
* @param {string[]} topActions
|
|
99
|
+
* @param {string | null | undefined} adopted
|
|
97
100
|
*/
|
|
98
|
-
export function isDoctorHealthyNothingToDo(designFitness, topActions = []) {
|
|
101
|
+
export function isDoctorHealthyNothingToDo(designFitness, topActions = [], adopted = null) {
|
|
99
102
|
if (designFitness?.designWeak) return false;
|
|
103
|
+
if (adopted !== 'required-merge') return false;
|
|
100
104
|
return !topActions.some(Boolean);
|
|
101
105
|
}
|
package/bin/lib/product-copy.mjs
CHANGED
|
@@ -13,17 +13,20 @@ export const LEFTOVER_DESIGN_LABEL = 'leftover design work';
|
|
|
13
13
|
* Operating-mode title for humans (and doctor JSON `designFitness.label` prefix).
|
|
14
14
|
* @param {string|null|undefined} mode suggest|adapt|enforce
|
|
15
15
|
* @param {boolean} leftoverDesign
|
|
16
|
+
* @param {boolean} [stewardsUnset]
|
|
16
17
|
*/
|
|
17
|
-
export function operatingModeTitle(mode, leftoverDesign) {
|
|
18
|
+
export function operatingModeTitle(mode, leftoverDesign, stewardsUnset) {
|
|
18
19
|
const light = String(mode || 'enforce').toUpperCase();
|
|
19
|
-
|
|
20
|
+
if (leftoverDesign) return `${light} · ${LEFTOVER_DESIGN_LABEL}`;
|
|
21
|
+
if (stewardsUnset) return `${light} · stewards unset`;
|
|
22
|
+
return light;
|
|
20
23
|
}
|
|
21
24
|
|
|
22
25
|
/** Short HTML/doctor badge text. */
|
|
23
26
|
export const LEFTOVER_DESIGN_BADGE = LEFTOVER_DESIGN_LABEL;
|
|
24
27
|
|
|
25
28
|
export const POST_GREEN_HUMAN =
|
|
26
|
-
'Imports check out, but the design is still messy.
|
|
29
|
+
'Imports check out, but the design is still messy. Next: /ark-explore, then one small refactor with /ark-autopilot and your OK.';
|
|
27
30
|
|
|
28
31
|
export const POST_GREEN_LEDE =
|
|
29
32
|
'Import rules are clean, but leftover design work remains. That does not fail the check — it only means “done” is still wrong until you tidy shape.';
|
|
@@ -151,7 +151,7 @@ export function renderStartPreview(preview, options = {}) {
|
|
|
151
151
|
console.log('Apply this plan with: arkgate start --apply');
|
|
152
152
|
}
|
|
153
153
|
if (preview.analysis) {
|
|
154
|
-
console.log(`Your project looks like: ${preview.analysis.label}
|
|
154
|
+
console.log(`Your project looks like: ${preview.analysis.label}.`);
|
|
155
155
|
}
|
|
156
156
|
console.log(applying ? 'Files create/edit/delete:' : 'Files to create/edit/delete:');
|
|
157
157
|
if (preview.changes.length === 0) console.log(' (none)');
|
|
@@ -159,34 +159,24 @@ export function renderStartPreview(preview, options = {}) {
|
|
|
159
159
|
console.log(` ${change.action.padEnd(6)} ${change.path}`);
|
|
160
160
|
}
|
|
161
161
|
if (!applying) {
|
|
162
|
-
console.log('
|
|
163
|
-
|
|
162
|
+
console.log('Setup: install package + host gates (see --json).');
|
|
163
|
+
console.log('Preview does not write. Apply installs CI.');
|
|
164
164
|
}
|
|
165
|
-
console.log('Host guarantees:');
|
|
166
|
-
for (const guarantee of preview.hostGuarantees) console.log(` ${guarantee}`);
|
|
167
165
|
if (preview.runtimeActivation) {
|
|
168
|
-
console.log('Codex
|
|
169
|
-
console.log(` Runtime activation: ${JSON.stringify(preview.runtimeActivation)}`);
|
|
170
|
-
console.log(` Restart Codex, then call ark_identity with expectedRoot "${preview.root}".`);
|
|
171
|
-
console.log(' Do not trust MCP verdicts before the project identity matches.');
|
|
166
|
+
console.log('Host: Codex is configured but not verified yet. Restart the host, then confirm this project.');
|
|
172
167
|
}
|
|
173
168
|
if (preview.unresolvedDecisions.length > 0) {
|
|
174
169
|
console.log('Unresolved decisions:');
|
|
175
170
|
for (const decision of preview.unresolvedDecisions) console.log(` ${decision}`);
|
|
176
171
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
);
|
|
186
|
-
for (const change of preview.changes) {
|
|
187
|
-
console.log(` ${change.action.padEnd(6)} ${change.path} ${change.afterHash ?? '(deleted)'}`);
|
|
188
|
-
}
|
|
189
|
-
if (!applying) {
|
|
172
|
+
if (applying) {
|
|
173
|
+
const percent = preview.projectedCoverage?.percent;
|
|
174
|
+
if (percent != null) console.log(`Projected governed coverage: ${percent}%`);
|
|
175
|
+
const verified = (preview.hostGuarantees || []).find((line) =>
|
|
176
|
+
String(line).startsWith('Hard-write hook verified')
|
|
177
|
+
);
|
|
178
|
+
if (verified) console.log(verified);
|
|
179
|
+
} else {
|
|
190
180
|
console.log('Review complete file contents with --json.');
|
|
191
181
|
}
|
|
192
182
|
}
|
|
@@ -26,6 +26,7 @@ import { readBaseline } from './violations.mjs';
|
|
|
26
26
|
import { reportsDir, readJsonSafe } from './html-report.mjs';
|
|
27
27
|
import { summarizeRulesUnderContract } from './rules-under-contract.mjs';
|
|
28
28
|
import { collectVsBaseFacts, discoverTeamBaseRef } from './team-parliament-io.mjs';
|
|
29
|
+
import { classifyAdopted, readAdoptionStance } from './adoption-stance.mjs';
|
|
29
30
|
|
|
30
31
|
function sha256Hex(value) {
|
|
31
32
|
return createHash('sha256').update(value, 'utf8').digest('hex');
|
|
@@ -387,6 +388,21 @@ export function collectStatusFacts(options = {}) {
|
|
|
387
388
|
latest?.leftoverDesignWork === true ||
|
|
388
389
|
latest?.designFitness?.designWeak === true ||
|
|
389
390
|
latest?.doctor?.designFitness?.designWeak === true,
|
|
391
|
+
adopted:
|
|
392
|
+
options.adopted ??
|
|
393
|
+
classifyAdopted({
|
|
394
|
+
stance: readAdoptionStance(resolvedRoot),
|
|
395
|
+
github: {
|
|
396
|
+
requiredStatusConfigured: writePath?.enforcementState?.ciMerge?.required === true,
|
|
397
|
+
arkCheckRequired: writePath?.enforcementState?.ciMerge?.required === true,
|
|
398
|
+
},
|
|
399
|
+
ci: {
|
|
400
|
+
state:
|
|
401
|
+
writePath?.enforcementState?.ciMerge?.required === true
|
|
402
|
+
? 'required'
|
|
403
|
+
: undefined,
|
|
404
|
+
},
|
|
405
|
+
}),
|
|
390
406
|
improvementCompass,
|
|
391
407
|
vsBase: (() => {
|
|
392
408
|
const vsRef = typeof options.vs === 'string' ? options.vs.trim() : '';
|
|
@@ -242,9 +242,15 @@ export function resolveStatusNextAction(facts, binding, activation, lastCheck, r
|
|
|
242
242
|
summary: 'ArkRules residual remains frozen — review inventory debt without claiming a score.',
|
|
243
243
|
};
|
|
244
244
|
}
|
|
245
|
+
if (facts.adopted === 'required-merge' || facts.adopted === 'advisory-only-acked') {
|
|
246
|
+
return {
|
|
247
|
+
id: 'stay-enforced',
|
|
248
|
+
summary: 'Contract looks enforceable for this session — keep writing through the gate and re-check after structural edits.',
|
|
249
|
+
};
|
|
250
|
+
}
|
|
245
251
|
return {
|
|
246
|
-
id: '
|
|
247
|
-
summary: '
|
|
252
|
+
id: 'require-ci-merge-status',
|
|
253
|
+
summary: 'Make arkgate-check --strict-merge a required GitHub status, or write .ark/adoption-stance.json with stance: "advisory-only".',
|
|
248
254
|
};
|
|
249
255
|
}
|
|
250
256
|
export function buildStatusManifest(facts) {
|
|
@@ -203,11 +203,30 @@ export function teamCheckRequested(args, config) {
|
|
|
203
203
|
args.against ||
|
|
204
204
|
args.persona ||
|
|
205
205
|
args.contractSession ||
|
|
206
|
+
args.updateBaseline ||
|
|
206
207
|
(args.strictMerge && teamStewardsFromConfig(config).length > 0)
|
|
207
208
|
);
|
|
208
209
|
}
|
|
209
210
|
|
|
210
211
|
export function runTeamPreflight({ root, args, config, policyDelta, teamBase }) {
|
|
212
|
+
const weakening =
|
|
213
|
+
policyDelta?.classification === 'weakening' ||
|
|
214
|
+
policyDelta?.classification === 'judgment-required';
|
|
215
|
+
if (weakening && !contractSessionFrom(args)) {
|
|
216
|
+
const message =
|
|
217
|
+
'Weakening the contract requires --contract-session (and --policy-ack bound to both hashes).';
|
|
218
|
+
const teamParliament = {
|
|
219
|
+
deny: true,
|
|
220
|
+
reasonId: 'steward-only-loosen',
|
|
221
|
+
message,
|
|
222
|
+
kinds: ['loosen'],
|
|
223
|
+
};
|
|
224
|
+
return {
|
|
225
|
+
halt: { exitCode: 1, message, teamParliament, fail: true },
|
|
226
|
+
teamParliament,
|
|
227
|
+
changedPaths: [],
|
|
228
|
+
};
|
|
229
|
+
}
|
|
211
230
|
if (!teamCheckRequested(args, config)) {
|
|
212
231
|
return { halt: null, teamParliament: null, changedPaths: [] };
|
|
213
232
|
}
|
|
@@ -301,7 +320,24 @@ export function applyAgainstRatchet({
|
|
|
301
320
|
};
|
|
302
321
|
}
|
|
303
322
|
|
|
323
|
+
/** Cheap doctor-path probe: skip git spawns on non-repos (hook-path bench tmpdirs). */
|
|
324
|
+
function gitDirPresent(root) {
|
|
325
|
+
let dir = path.resolve(root);
|
|
326
|
+
for (let i = 0; i < 10; i += 1) {
|
|
327
|
+
try {
|
|
328
|
+
if (fs.existsSync(path.join(dir, '.git'))) return true;
|
|
329
|
+
} catch {
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
const parent = path.dirname(dir);
|
|
333
|
+
if (parent === dir) break;
|
|
334
|
+
dir = parent;
|
|
335
|
+
}
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
|
|
304
339
|
function gitAuthors(root) {
|
|
340
|
+
if (!gitDirPresent(root)) return [];
|
|
305
341
|
const log = runGit(root, ['log', '--format=%aN<%aE>', '--max-count=300']);
|
|
306
342
|
if (log.status !== 0) return [];
|
|
307
343
|
const ids = [];
|
|
@@ -328,11 +364,35 @@ function readCodeowners(root) {
|
|
|
328
364
|
return [];
|
|
329
365
|
}
|
|
330
366
|
|
|
331
|
-
|
|
332
|
-
|
|
367
|
+
function gitFirstAddIso(root, relPath) {
|
|
368
|
+
if (!gitDirPresent(root)) return null;
|
|
369
|
+
const log = runGit(root, ['log', '--diff-filter=A', '--follow', '--format=%cI', '--', relPath]);
|
|
370
|
+
if (log.status !== 0) return null;
|
|
371
|
+
const lines = log.stdout
|
|
372
|
+
.split('\n')
|
|
373
|
+
.map((line) => line.trim())
|
|
374
|
+
.filter(Boolean);
|
|
375
|
+
return lines.length > 0 ? lines[lines.length - 1] : null;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/** Tooling clock: git first-add of ark.config.json vs injected `now`. Domain never clocks. */
|
|
379
|
+
export function adoptAgeDaysFromGit(root, relPath, now) {
|
|
380
|
+
const iso = gitFirstAddIso(root, relPath);
|
|
381
|
+
if (!iso) return { days: null, source: 'unavailable' };
|
|
382
|
+
const then = Date.parse(iso);
|
|
383
|
+
const nowMs = now instanceof Date ? now.getTime() : Number(now);
|
|
384
|
+
if (!Number.isFinite(then) || !Number.isFinite(nowMs)) return { days: null, source: 'unavailable' };
|
|
385
|
+
return { days: Math.floor((nowMs - then) / 86_400_000), source: 'git-first-add' };
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/** Advisory residual. Never flips `valid` / `goal.met`. Missing git is unknown age, not green. */
|
|
389
|
+
export function collectStewardNudge(root, config, options = {}) {
|
|
390
|
+
const now = options.now instanceof Date ? options.now : options.now != null ? new Date(options.now) : new Date();
|
|
391
|
+
const age = adoptAgeDaysFromGit(root, options.configRel || 'ark.config.json', now);
|
|
333
392
|
return suggestStewards({
|
|
334
393
|
existingStewards: teamStewardsFromConfig(config),
|
|
335
394
|
gitAuthors: gitAuthors(root),
|
|
336
395
|
codeowners: readCodeowners(root),
|
|
396
|
+
adoptAgeDays: age.days,
|
|
337
397
|
});
|
|
338
398
|
}
|
|
@@ -198,8 +198,9 @@ export function parseCodeownersHandles(text) {
|
|
|
198
198
|
}
|
|
199
199
|
/**
|
|
200
200
|
* Empty list + several hands → propose owners.
|
|
201
|
+
* Empty list + grace elapsed or unknown age → unfinished residual (not a new operating mode).
|
|
201
202
|
* Existing list + CODEOWNERS ahead or author count grew → show the gap.
|
|
202
|
-
* Never a
|
|
203
|
+
* Never a layer / `valid` / `goal.met` input. Propose GitHub handles or emails — never git display names.
|
|
203
204
|
*/
|
|
204
205
|
export function suggestStewards(input) {
|
|
205
206
|
const existing = [
|
|
@@ -224,7 +225,9 @@ export function suggestStewards(input) {
|
|
|
224
225
|
];
|
|
225
226
|
const authorCount = Math.max(uniqueGit.length, fromOwners.length);
|
|
226
227
|
const multiHand = uniqueGit.length >= 2 || fromOwners.length >= 1;
|
|
227
|
-
const
|
|
228
|
+
const age = input.adoptAgeDays;
|
|
229
|
+
const emptyStewardsPastGrace = existing.length === 0 && (age === null || (typeof age === 'number' && age >= 30));
|
|
230
|
+
const needsStewards = existing.length === 0 && (multiHand || emptyStewardsPastGrace);
|
|
228
231
|
const missingFromList = fromOwners.filter((id) => !existing.includes(id));
|
|
229
232
|
const teamGrew = existing.length > 0 &&
|
|
230
233
|
fromOwners.length === 0 &&
|
|
@@ -238,24 +241,29 @@ export function suggestStewards(input) {
|
|
|
238
241
|
const named = proposed.map((id) => formatStewardMention(id)).join(', ');
|
|
239
242
|
const listed = existing.map((id) => formatStewardMention(id)).join(', ');
|
|
240
243
|
let ask = '';
|
|
241
|
-
if (needsStewards) {
|
|
244
|
+
if (needsStewards && multiHand) {
|
|
242
245
|
ask =
|
|
243
246
|
proposed.length > 0
|
|
244
247
|
? `This repo has several people and no stewards. Add ${named} as stewards so only they can loosen the law or grow the baseline? Say yes, or name the GitHub handles or emails.`
|
|
245
248
|
: 'This repo has several people and no stewards. Who owns ark.config.json? Name GitHub handles or emails for the stewards[] list.';
|
|
246
249
|
}
|
|
250
|
+
else if (needsStewards) {
|
|
251
|
+
ask =
|
|
252
|
+
'No stewards listed. Name GitHub handles or emails for `stewards[]`, or this stays Adapt-or-nudge — not a finished Enforce. `/ark-adopt` asks; it does not invent names.';
|
|
253
|
+
}
|
|
247
254
|
else if (missingFromList.length > 0) {
|
|
248
255
|
ask = `CODEOWNERS is ahead of stewards[]: add ${missingFromList.map((id) => formatStewardMention(id)).join(', ')}? The current list stays unless you say yes or name the GitHub handles or emails.`;
|
|
249
256
|
}
|
|
250
257
|
else if (teamGrew) {
|
|
251
258
|
ask = `This repo started with ${existing.length} steward(s) (${listed}) and now has ${uniqueGit.length} recent git authors. Who else owns the law? Name GitHub handles or emails, or say the list is still right.`;
|
|
252
259
|
}
|
|
253
|
-
const shouldAct = needsStewards || drift;
|
|
260
|
+
const shouldAct = needsStewards || drift || emptyStewardsPastGrace;
|
|
254
261
|
return {
|
|
255
262
|
advisory: true,
|
|
256
263
|
notAScore: true,
|
|
257
264
|
multiHand,
|
|
258
265
|
needsStewards,
|
|
266
|
+
emptyStewardsPastGrace,
|
|
259
267
|
drift,
|
|
260
268
|
authorCount,
|
|
261
269
|
stewardCount: existing.length,
|
|
@@ -266,6 +274,7 @@ export function suggestStewards(input) {
|
|
|
266
274
|
nextAction: shouldAct
|
|
267
275
|
? '/ark-adopt (ask, then update stewards[] — do not invent names)'
|
|
268
276
|
: '',
|
|
277
|
+
adoptAgeDays: typeof age === 'number' ? age : null,
|
|
269
278
|
};
|
|
270
279
|
}
|
|
271
280
|
export function personaCheckBudget(persona) {
|
|
@@ -310,7 +319,8 @@ export function isTeamPersona(value) {
|
|
|
310
319
|
/**
|
|
311
320
|
* Gate for a diff vs the merge base.
|
|
312
321
|
* Contract session still forbids mixing law with product source.
|
|
313
|
-
* Loosen / baseline-grow require
|
|
322
|
+
* Loosen / baseline-grow require --contract-session even when stewards is empty.
|
|
323
|
+
* A non-empty list additionally requires a matching listed author.
|
|
314
324
|
*/
|
|
315
325
|
export function evaluateTeamGate(input) {
|
|
316
326
|
const kinds = [];
|
|
@@ -338,6 +348,16 @@ export function evaluateTeamGate(input) {
|
|
|
338
348
|
const stewards = input.stewards ?? [];
|
|
339
349
|
const grow = (input.baselineGrowCount ?? 0) > 0;
|
|
340
350
|
const loosen = input.policyKind === 'loosen';
|
|
351
|
+
if ((loosen || grow) && !input.contractSession) {
|
|
352
|
+
return {
|
|
353
|
+
deny: true,
|
|
354
|
+
reasonId: loosen ? 'steward-only-loosen' : 'steward-only-baseline-grow',
|
|
355
|
+
message: loosen
|
|
356
|
+
? 'Weakening the contract requires --contract-session (and --policy-ack bound to both hashes).'
|
|
357
|
+
: 'Growing the baseline requires --contract-session. Freeze in a law-only PR.',
|
|
358
|
+
kinds,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
341
361
|
if (stewards.length > 0 && (loosen || grow) && !isSteward(input.author, stewards)) {
|
|
342
362
|
return {
|
|
343
363
|
deny: true,
|