akm-cli 0.9.18 → 0.9.19-alpha.1
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 +135 -0
- package/STABILITY.md +2 -1
- package/dist/assets/hints/cli-hints-full.md +4 -2
- package/dist/assets/hints/cli-hints-short.md +5 -3
- package/dist/assets/prompts/reflect-feedback-framing.md +1 -1
- package/dist/commands/feedback-cli.js +1 -1
- package/dist/commands/improve/consolidate/coverage.js +132 -0
- package/dist/commands/improve/consolidate/pair-pass.js +30 -20
- package/dist/commands/improve/consolidate.js +43 -10
- package/dist/commands/improve/distill.js +7 -3
- package/dist/commands/improve/eligibility.js +39 -9
- package/dist/commands/improve/improve-cli.js +9 -6
- package/dist/commands/improve/improve.js +13 -4
- package/dist/commands/improve/ledger.js +2 -2
- package/dist/commands/improve/loop-stages.js +2 -0
- package/dist/commands/improve/preparation.js +1 -1
- package/dist/commands/improve/reflect.js +1 -1
- package/dist/commands/improve/stage.js +38 -9
- package/dist/commands/proposal/diff-format.js +21 -0
- package/dist/commands/proposal/proposal-cli.js +48 -10
- package/dist/commands/proposal/proposal-types.js +11 -0
- package/dist/commands/proposal/proposal.js +60 -5
- package/dist/commands/proposal/repository.js +245 -17
- package/dist/commands/read/knowledge.js +13 -11
- package/dist/commands/read/remember-cli.js +7 -3
- package/dist/commands/sources/source-clone.js +1 -1
- package/dist/commands/tasks/tasks-cli.js +1 -1
- package/dist/commands/tasks/tasks.js +10 -3
- package/dist/core/mutation-target.js +8 -3
- package/dist/core/write-source.js +3 -2
- package/dist/indexer/usage/usage-events.js +2 -1
- package/dist/output/shapes/helpers.js +7 -0
- package/dist/output/shapes/passthrough.js +1 -0
- package/dist/output/shapes/proposal/reopen.js +14 -0
- package/dist/output/shapes.js +2 -0
- package/dist/output/text/helpers.js +1 -1
- package/dist/output/text/proposal/proposal.js +3 -1
- package/dist/output/text/proposal-format.js +87 -32
- package/dist/scripts/akm-migrate-node.js +79 -23
- package/dist/scripts/akm-migrate.js +79 -23
- package/dist/storage/repositories/improve-ledger-repository.js +65 -6
- package/dist/storage/repositories/index-vec-repository.js +13 -8
- package/dist/storage/repositories/proposals-repository.js +23 -0
- package/dist/tasks/run/load-task.js +5 -1
- package/docs/migration/README.md +1 -0
- package/docs/migration/release-notes/0.9.19.md +134 -0
- package/docs/migration/release-notes/README.md +5 -0
- package/docs/migration/v0.8-to-v0.9.md +5 -1
- package/docs/reference/cli.md +189 -28
- package/docs/reference/configuration.md +9 -8
- package/docs/reference/data-and-telemetry.md +19 -14
- package/package.json +1 -1
|
@@ -21,7 +21,7 @@ import { assembleAsset, serializeFrontmatter } from "../../core/asset/asset-seri
|
|
|
21
21
|
import { carryForwardBookkeepingFrontmatter, parseFrontmatter } from "../../core/asset/frontmatter.js";
|
|
22
22
|
import { conceptIdFromTypeName, parseRefInput } from "../../core/asset/resolve-ref.js";
|
|
23
23
|
import { loadConfig } from "../../core/config/config.js";
|
|
24
|
-
import { ConfigError, NotFoundError, UsageError } from "../../core/errors.js";
|
|
24
|
+
import { ConfigError, NotFoundError, rethrowIfTestIsolationError, UsageError } from "../../core/errors.js";
|
|
25
25
|
import { appendEvent } from "../../core/events.js";
|
|
26
26
|
import { proposalContent } from "../../core/file-change.js";
|
|
27
27
|
import { canonicalBundleIdForTarget, resolveBundleWriteTarget } from "../../core/mutation-target.js";
|
|
@@ -34,7 +34,7 @@ import { indexWrittenAssets } from "../../indexer/index-written-assets.js";
|
|
|
34
34
|
import { deriveInstallations } from "../../indexer/installations.js";
|
|
35
35
|
import { resolveSourceEntries } from "../../indexer/search/search-source.js";
|
|
36
36
|
import { insertEventOnce } from "../../storage/repositories/events-repository.js";
|
|
37
|
-
import { recordImproveLedger, recordImproveLedgerDecision, } from "../../storage/repositories/improve-ledger-repository.js";
|
|
37
|
+
import { CONSOLIDATE_LEDGER_SOURCE, forgetImproveLedgerDecision, recordImproveLedger, recordImproveLedgerDecision, reopenImproveLedgerDecision, } from "../../storage/repositories/improve-ledger-repository.js";
|
|
38
38
|
import { getStateProposal, listStateProposalIdsByPrefix, listStateProposals, upsertProposal, } from "../../storage/repositories/proposals-repository.js";
|
|
39
39
|
import { openSqliteReadSnapshot } from "../../storage/sqlite-read-snapshot.js";
|
|
40
40
|
import { pkgVersion } from "../../version.js";
|
|
@@ -42,8 +42,8 @@ import { contentHash } from "../improve/content-hash.js";
|
|
|
42
42
|
import { writeSupersededEdge } from "../improve/memory/memory-belief.js";
|
|
43
43
|
import { archiveCleanupCandidate, derivedTwinPath } from "../improve/memory/memory-improve.js";
|
|
44
44
|
import { runBaseChecks } from "../lint/base-linter.js";
|
|
45
|
-
import { formatNewAssetDiff, formatUnifiedDiff } from "./diff-format.js";
|
|
46
|
-
import { ASSET_MISSING_GATE_REASON, EXPIRED_GATE_REASON, isAutomatedProposalSource, isRetireProposal, isValidProposalSource, PROPOSAL_SOURCES, STALE_TARGET_GATE_REASON, } from "./proposal-types.js";
|
|
45
|
+
import { formatNewAssetDiff, formatRetireDiff, formatUnifiedDiff } from "./diff-format.js";
|
|
46
|
+
import { ASSET_MISSING_GATE_REASON, EXPIRED_GATE_REASON, isAutomatedProposalSource, isRetireProposal, isValidProposalSource, PROPOSAL_SOURCES, proposalWaitingSince, STALE_TARGET_GATE_REASON, } from "./proposal-types.js";
|
|
47
47
|
import { canonicalOnlyProposalValidators, hasCanonicalProposalValidator, runProposalValidators, } from "./validators/proposal-validators.js";
|
|
48
48
|
import { repairProposalContent, validateProposal } from "./validators/proposals.js";
|
|
49
49
|
export { AUTOMATED_PROPOSAL_SOURCES, isAutomatedProposalSource, isValidProposalSource, PROPOSAL_SOURCES, } from "./proposal-types.js";
|
|
@@ -141,6 +141,28 @@ export function resolveProposalQueueTarget(stashDir, config = loadConfig()) {
|
|
|
141
141
|
}
|
|
142
142
|
return { source: bundleId, root };
|
|
143
143
|
}
|
|
144
|
+
/**
|
|
145
|
+
* The configured bundle that owns the asset `itemRef` names when it is not the
|
|
146
|
+
* bundle rooted at `target`; otherwise `undefined`. A differing name is
|
|
147
|
+
* confirmed by the owner's root, so one bundle known by two spellings is never
|
|
148
|
+
* taken for two, and an owner that cannot be resolved is never refused on.
|
|
149
|
+
*/
|
|
150
|
+
function otherOwningBundle(itemRef, target) {
|
|
151
|
+
try {
|
|
152
|
+
const owner = parseBundleRef(itemRef).bundle;
|
|
153
|
+
if (owner === undefined || owner === target.source)
|
|
154
|
+
return undefined;
|
|
155
|
+
const sources = resolveSourceEntries(target.root, loadConfig());
|
|
156
|
+
const ownerSource = sources[deriveInstallations(sources).findIndex((installation) => installation.id === owner)];
|
|
157
|
+
return ownerSource !== undefined && path.resolve(ownerSource.path) !== path.resolve(target.root)
|
|
158
|
+
? owner
|
|
159
|
+
: undefined;
|
|
160
|
+
}
|
|
161
|
+
catch (err) {
|
|
162
|
+
rethrowIfTestIsolationError(err);
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
144
166
|
/**
|
|
145
167
|
* Create a pending proposal (a random UUID id). Obviously invalid input is
|
|
146
168
|
* refused with a typed `proposal_creation_rejected` event. The mint and its
|
|
@@ -190,9 +212,13 @@ export function createProposal(stashDir, input, ctx) {
|
|
|
190
212
|
return rejectProposal("missing_description", `Proposal for "${input.ref}" (source=consolidate) has empty or missing frontmatter description.`);
|
|
191
213
|
}
|
|
192
214
|
}
|
|
215
|
+
const proposalTarget = resolveCreateProposalTarget(stashDir, input.target, parsedRef.origin);
|
|
216
|
+
const owner = input.itemRef ? otherOwningBundle(input.itemRef, proposalTarget) : undefined;
|
|
217
|
+
if (owner) {
|
|
218
|
+
return rejectProposal("cross_bundle", `Proposal for "${input.ref}" rewrites ${input.itemRef}, which bundle "${owner}" owns, but its queue target is bundle "${proposalTarget.source}". Filing it there would fork the asset out of "${owner}" or overwrite this bundle's copy with content taken from the other. Improve it in its own bundle instead (\`akm improve --bundle ${owner}\`).`);
|
|
219
|
+
}
|
|
193
220
|
// The FileChange envelope, and the target's before-hashes as of mint (the
|
|
194
221
|
// freshness check at accept compares against them).
|
|
195
|
-
const proposalTarget = resolveCreateProposalTarget(stashDir, input.target, parsedRef.origin);
|
|
196
222
|
const normalizedRef = proposalDurableRef(parsedRef, proposalTarget);
|
|
197
223
|
const targetRoot = path.resolve(proposalTarget.root);
|
|
198
224
|
let targetRelPath;
|
|
@@ -454,6 +480,25 @@ function ledgerOutcomeForDecision(status, gateDecision) {
|
|
|
454
480
|
}
|
|
455
481
|
return "rejected";
|
|
456
482
|
}
|
|
483
|
+
/**
|
|
484
|
+
* What a decision's ledger row is about. A promotion's row is keyed by its
|
|
485
|
+
* source memory, so a decision on it names that memory, together with the body
|
|
486
|
+
* hash the promotion was queued against — this is what holds the memory back
|
|
487
|
+
* until it changes (`isContentDrivenDecision`). Any other proposal, and a
|
|
488
|
+
* promotion minted before the hash was recorded, is keyed by its own ref.
|
|
489
|
+
* Every decision that can create a row when none carries the proposal id — a
|
|
490
|
+
* reject, an accept and a drain deferral alike — must key it this way: a stray
|
|
491
|
+
* row under the knowledge ref carries the proposal id, so a later verdict
|
|
492
|
+
* updates that one and never reaches the memory's own row.
|
|
493
|
+
*/
|
|
494
|
+
function decisionLedgerSubject(proposal) {
|
|
495
|
+
if (proposal.source === CONSOLIDATE_LEDGER_SOURCE &&
|
|
496
|
+
proposal.promotionSource !== undefined &&
|
|
497
|
+
proposal.promotionSourceHash !== undefined) {
|
|
498
|
+
return { ref: proposal.promotionSource, contentHash: proposal.promotionSourceHash };
|
|
499
|
+
}
|
|
500
|
+
return { ref: proposal.ref };
|
|
501
|
+
}
|
|
457
502
|
/** Archive a pending proposal as accepted/rejected, recording the decision in the ledger in the same transaction. */
|
|
458
503
|
export function archiveProposal(stashDir, id, status, reason, ctx, gateDecision) {
|
|
459
504
|
return withProposalsDb(ctx, (db) => withImmediateTransaction(db, () => {
|
|
@@ -473,7 +518,7 @@ export function archiveProposal(stashDir, id, status, reason, ctx, gateDecision)
|
|
|
473
518
|
recordImproveLedgerDecision(db, {
|
|
474
519
|
proposalId: updated.id,
|
|
475
520
|
stashDir,
|
|
476
|
-
|
|
521
|
+
...decisionLedgerSubject(updated),
|
|
477
522
|
source: updated.source,
|
|
478
523
|
outcome: ledgerOutcomeForDecision(status, gateDecision),
|
|
479
524
|
at: decidedAt,
|
|
@@ -500,7 +545,7 @@ export function recordGateDecision(stashDir, id, decision, ctx) {
|
|
|
500
545
|
recordImproveLedgerDecision(db, {
|
|
501
546
|
proposalId: updated.id,
|
|
502
547
|
stashDir,
|
|
503
|
-
|
|
548
|
+
...decisionLedgerSubject(updated),
|
|
504
549
|
source: updated.source,
|
|
505
550
|
outcome: "review_needed",
|
|
506
551
|
at: decidedAt,
|
|
@@ -551,9 +596,10 @@ export function purgeOrphanProposals(stashDir, sourceDirs, ctx) {
|
|
|
551
596
|
}
|
|
552
597
|
/**
|
|
553
598
|
* Archive pending proposals older than `archiveRetentionDays` (default 90;
|
|
554
|
-
* 0 disables) as rejected with an
|
|
555
|
-
* `
|
|
556
|
-
* the rejection window, since nobody judged the
|
|
599
|
+
* 0 disables; counted from the last reopen, if any) as rejected with an
|
|
600
|
+
* `expired` gate decision and a `proposal_expired` event. The ledger records
|
|
601
|
+
* `expired` — a short grace, not the rejection window, since nobody judged the
|
|
602
|
+
* content.
|
|
557
603
|
*/
|
|
558
604
|
export function expireStaleProposals(stashDir, config, ctx) {
|
|
559
605
|
const t0 = Date.now();
|
|
@@ -572,7 +618,9 @@ export function expireStaleProposals(stashDir, config, ctx) {
|
|
|
572
618
|
// way back short of the pair pass finding it again from scratch.
|
|
573
619
|
if (isRetireProposal(p))
|
|
574
620
|
continue;
|
|
575
|
-
|
|
621
|
+
// A reopened proposal's wait starts over at the reopen (#997): expiring it
|
|
622
|
+
// on its original age would undo the reopen at the next sweep.
|
|
623
|
+
const createdMs = new Date(proposalWaitingSince(p)).getTime();
|
|
576
624
|
if (!Number.isFinite(createdMs) || nowMs - createdMs < retentionDays * MS_PER_DAY)
|
|
577
625
|
continue;
|
|
578
626
|
try {
|
|
@@ -690,7 +738,7 @@ function persistProposalDecision(stashDir, proposal, decision, ctx) {
|
|
|
690
738
|
recordImproveLedgerDecision(db, {
|
|
691
739
|
proposalId: next.id,
|
|
692
740
|
stashDir,
|
|
693
|
-
|
|
741
|
+
...decisionLedgerSubject(next),
|
|
694
742
|
source: next.source,
|
|
695
743
|
outcome: ledgerOutcomeForDecision(accept ? "accepted" : "reverted"),
|
|
696
744
|
at: decision.decidedAt,
|
|
@@ -1591,17 +1639,197 @@ async function revertProposalWithLease(stashDir, config, id, options, ctx) {
|
|
|
1591
1639
|
await indexWrittenProposalAsset(target, assetPath);
|
|
1592
1640
|
return { proposal: reverted, assetPath, ref: proposal.ref };
|
|
1593
1641
|
}
|
|
1642
|
+
/** The message of the stale-target refusal `check` raises, else `undefined`. */
|
|
1643
|
+
function staleRefusal(check) {
|
|
1644
|
+
try {
|
|
1645
|
+
check();
|
|
1646
|
+
return undefined;
|
|
1647
|
+
}
|
|
1648
|
+
catch (error) {
|
|
1649
|
+
if (error instanceof UsageError)
|
|
1650
|
+
return error.message;
|
|
1651
|
+
throw error;
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
/**
|
|
1655
|
+
* Why `proposal` cannot be reopened, or `undefined` when it can. Only a
|
|
1656
|
+
* rejected proposal comes back, and only one accept would not refuse as stale:
|
|
1657
|
+
* a retire proposal needs its successor and both recorded body hashes to still
|
|
1658
|
+
* match (B2), any other its target to be what it was minted against
|
|
1659
|
+
* (STALE, R20) — so a reopened proposal is never one the next accept refuses.
|
|
1660
|
+
*/
|
|
1661
|
+
function reopenRefusal(config, proposal, queueTarget) {
|
|
1662
|
+
if (proposal.status !== "rejected") {
|
|
1663
|
+
return `it is not rejected (current status: ${proposal.status}); only a rejected proposal can be reopened.`;
|
|
1664
|
+
}
|
|
1665
|
+
if (proposal.changes.length === 0 || proposal.proposedTarget === undefined) {
|
|
1666
|
+
// A pending row must carry both (proposalToRowValues), which a row from
|
|
1667
|
+
// before the change envelope existed cannot.
|
|
1668
|
+
return "it was recorded before proposals carried their change envelope, so it cannot go back in the queue.";
|
|
1669
|
+
}
|
|
1670
|
+
const target = resolveProposalWriteTarget(config, proposal, undefined, queueTarget);
|
|
1671
|
+
const assetPath = resolveAssetFilePathSafe(target.source, parseRefInput(proposal.ref));
|
|
1672
|
+
if (!assetPath)
|
|
1673
|
+
return "its target cannot be resolved.";
|
|
1674
|
+
if (!isRetireProposal(proposal)) {
|
|
1675
|
+
return staleRefusal(() => void readFreshProposalTarget(proposal, assetPath, proposalContent(proposal)));
|
|
1676
|
+
}
|
|
1677
|
+
if (!proposal.retirement)
|
|
1678
|
+
return "it has no retirement metadata.";
|
|
1679
|
+
if (!fs.existsSync(assetPath)) {
|
|
1680
|
+
return "its retired file no longer exists (already retired, or removed by something else).";
|
|
1681
|
+
}
|
|
1682
|
+
const { retirement } = proposal;
|
|
1683
|
+
return staleRefusal(() => assertRetirementStillFresh(proposal.id, proposal.ref, retirement, target.source, fs.readFileSync(assetPath)));
|
|
1684
|
+
}
|
|
1685
|
+
/** The assets a retire proposal speaks for — the one it retires and its successor — as bundle-less concept ids, the way the pair pass keys them. */
|
|
1686
|
+
function retireHeldRefs(proposal) {
|
|
1687
|
+
return [proposal.ref, proposal.retirement?.successorRef].flatMap((ref) => {
|
|
1688
|
+
const conceptId = ref === undefined ? undefined : proposalRefIdentity(ref)?.conceptId;
|
|
1689
|
+
return conceptId === undefined ? [] : [conceptId];
|
|
1690
|
+
});
|
|
1691
|
+
}
|
|
1692
|
+
const REOPEN_REFUSED_HINT = "Only a rejected proposal whose target is unchanged can be reopened; `akm proposal list --status rejected` lists the candidates.";
|
|
1693
|
+
const REOPEN_RETIRE_CONFLICT_HINT = "Only one pending retire proposal can involve a document: accept or reject the pending one (or reopen just one of a clashing pair), then reopen the other.";
|
|
1694
|
+
/**
|
|
1695
|
+
* The pair pass never has two pending retire proposals speak for one asset
|
|
1696
|
+
* (`pendingRetireRefs`): accepting one would strand the other. Reopening must
|
|
1697
|
+
* not break that either — a rejected pair can meanwhile have been re-paired
|
|
1698
|
+
* with something else. `held` maps each asset to the pending retire proposal
|
|
1699
|
+
* that has it; a proposal that clears this claims its assets, so two in one
|
|
1700
|
+
* batch that clash refuse the later.
|
|
1701
|
+
*/
|
|
1702
|
+
function retireConflict(proposal, held) {
|
|
1703
|
+
if (!isRetireProposal(proposal))
|
|
1704
|
+
return undefined;
|
|
1705
|
+
const refs = retireHeldRefs(proposal);
|
|
1706
|
+
for (const ref of refs) {
|
|
1707
|
+
const holder = held.get(ref);
|
|
1708
|
+
if (holder !== undefined) {
|
|
1709
|
+
return `${ref} is already part of retire proposal ${holder}, which is pending or being reopened with this one; only one pending retire proposal may involve an asset.`;
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
for (const ref of refs)
|
|
1713
|
+
held.set(ref, proposal.id);
|
|
1714
|
+
return undefined;
|
|
1715
|
+
}
|
|
1716
|
+
/**
|
|
1717
|
+
* Put rejected proposals back in the queue (`akm proposal reopen`, #997): a
|
|
1718
|
+
* rejection is otherwise final, and it also suppresses the pair pass from ever
|
|
1719
|
+
* re-proposing a retirement (its record keys the pair), so a mistaken one
|
|
1720
|
+
* could not be undone. Each proposal returns to `pending` with the rejection —
|
|
1721
|
+
* and the gate verdict that came with it — kept in `reviewHistory` (the verdict
|
|
1722
|
+
* itself is cleared, unless it is a `deferred` hand-off to a person), its
|
|
1723
|
+
* ledger row reset (the pair pass keys off proposal status, so the pair is no
|
|
1724
|
+
* longer suppressed and, while pending, cannot be minted twice), and a
|
|
1725
|
+
* `proposal_reopened` event recorded, all in one transaction.
|
|
1726
|
+
*
|
|
1727
|
+
* All-or-nothing: every id is checked (see {@link reopenRefusal} and
|
|
1728
|
+
* {@link retireConflict}) before any is reopened, and one refusal leaves the
|
|
1729
|
+
* whole batch untouched.
|
|
1730
|
+
*/
|
|
1731
|
+
export function reopenProposals(stashDir, config, ids, options = {}, ctx) {
|
|
1732
|
+
return withProposalsDb(ctx, (db) => withImmediateTransaction(db, () => {
|
|
1733
|
+
const proposals = [...new Set(ids)].map((id) => requireProposal(db, stashDir, id));
|
|
1734
|
+
const held = new Map();
|
|
1735
|
+
for (const pending of listStateProposals(db, { stashDir, status: "pending" })) {
|
|
1736
|
+
if (isRetireProposal(pending))
|
|
1737
|
+
for (const ref of retireHeldRefs(pending))
|
|
1738
|
+
held.set(ref, pending.id);
|
|
1739
|
+
}
|
|
1740
|
+
const refusals = proposals.flatMap((proposal) => {
|
|
1741
|
+
const refusal = reopenRefusal(config, proposal, options.queueTarget);
|
|
1742
|
+
const conflict = refusal === undefined ? retireConflict(proposal, held) : undefined;
|
|
1743
|
+
const reason = refusal ?? conflict;
|
|
1744
|
+
return reason === undefined
|
|
1745
|
+
? []
|
|
1746
|
+
: [{ proposal: `${proposal.id} (${proposal.ref})`, reason, isConflict: conflict !== undefined }];
|
|
1747
|
+
});
|
|
1748
|
+
if (refusals.length > 0) {
|
|
1749
|
+
// A retire conflict is not about the target changing, so it gets its own hint.
|
|
1750
|
+
const hints = [
|
|
1751
|
+
...(refusals.some((refusal) => !refusal.isConflict) ? [REOPEN_REFUSED_HINT] : []),
|
|
1752
|
+
...(refusals.some((refusal) => refusal.isConflict) ? [REOPEN_RETIRE_CONFLICT_HINT] : []),
|
|
1753
|
+
];
|
|
1754
|
+
throw new UsageError(proposals.length === 1
|
|
1755
|
+
? `Proposal ${refusals[0]?.proposal} cannot be reopened: ${refusals[0]?.reason}`
|
|
1756
|
+
: `Cannot reopen ${refusals.length} of ${proposals.length} proposals; none were reopened:\n${refusals
|
|
1757
|
+
.map((refusal) => ` - ${refusal.proposal}: ${refusal.reason}`)
|
|
1758
|
+
.join("\n")}`, "INVALID_FLAG_VALUE", hints.join(" "));
|
|
1759
|
+
}
|
|
1760
|
+
const decidedAt = nowIso(ctx);
|
|
1761
|
+
return proposals.map((existing) => {
|
|
1762
|
+
const reopened = {
|
|
1763
|
+
...existing,
|
|
1764
|
+
status: "pending",
|
|
1765
|
+
updatedAt: decidedAt,
|
|
1766
|
+
review: undefined,
|
|
1767
|
+
// A reopened proposal is adjudicated afresh: a `staged` verdict would
|
|
1768
|
+
// let the drain accept it unseen, and another gate's `auto-rejected`
|
|
1769
|
+
// would have the drain skip it. A `deferred` one is the quality
|
|
1770
|
+
// gate's hand-off to a person, which the drain must keep honouring
|
|
1771
|
+
// (drainProposals leaves it alone), so it stays.
|
|
1772
|
+
gateDecision: existing.gateDecision?.outcome === "deferred" ? existing.gateDecision : undefined,
|
|
1773
|
+
reviewHistory: [
|
|
1774
|
+
...(existing.reviewHistory ?? []),
|
|
1775
|
+
{
|
|
1776
|
+
...(existing.review !== undefined ? { review: existing.review } : {}),
|
|
1777
|
+
...(existing.gateDecision !== undefined ? { gateDecision: existing.gateDecision } : {}),
|
|
1778
|
+
reopenedAt: decidedAt,
|
|
1779
|
+
...(options.reason !== undefined ? { reopenReason: options.reason } : {}),
|
|
1780
|
+
},
|
|
1781
|
+
],
|
|
1782
|
+
};
|
|
1783
|
+
upsertProposal(db, reopened, stashDir);
|
|
1784
|
+
if (isRetireProposal(existing)) {
|
|
1785
|
+
// A retire mint writes no ledger row, so the one its rejection wrote goes.
|
|
1786
|
+
forgetImproveLedgerDecision(db, stashDir, existing.id);
|
|
1787
|
+
}
|
|
1788
|
+
else {
|
|
1789
|
+
reopenImproveLedgerDecision(db, {
|
|
1790
|
+
proposalId: existing.id,
|
|
1791
|
+
stashDir,
|
|
1792
|
+
source: existing.source,
|
|
1793
|
+
at: decidedAt,
|
|
1794
|
+
detail: options.reason !== undefined ? `reopened: ${options.reason}` : "reopened",
|
|
1795
|
+
});
|
|
1796
|
+
}
|
|
1797
|
+
insertEventOnce(db, {
|
|
1798
|
+
eventType: "proposal_reopened",
|
|
1799
|
+
ts: decidedAt,
|
|
1800
|
+
ref: reopened.ref,
|
|
1801
|
+
metadata: {
|
|
1802
|
+
proposalId: reopened.id,
|
|
1803
|
+
source: reopened.source,
|
|
1804
|
+
...(reopened.sourceRun !== undefined ? { sourceRun: reopened.sourceRun } : {}),
|
|
1805
|
+
...(options.reason !== undefined ? { reason: options.reason } : {}),
|
|
1806
|
+
},
|
|
1807
|
+
idempotencyKey: `${reopened.id}:reopened:${decidedAt}`,
|
|
1808
|
+
});
|
|
1809
|
+
return reopened;
|
|
1810
|
+
});
|
|
1811
|
+
}));
|
|
1812
|
+
}
|
|
1594
1813
|
/** The proposal against the asset its accept would overwrite (same target resolution as accept). */
|
|
1595
1814
|
export function diffProposal(stashDir, config, id, options = {}, ctx) {
|
|
1596
1815
|
const proposal = getProposal(stashDir, id, ctx);
|
|
1597
1816
|
const target = resolveProposalWriteTarget(config, proposal, options.target, options.queueTarget);
|
|
1598
1817
|
const targetPath = resolveAssetFilePathSafe(target.source, parseRefInput(proposal.ref));
|
|
1599
1818
|
const existing = targetPath && fs.existsSync(targetPath) ? fs.readFileSync(targetPath, "utf8") : null;
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1819
|
+
if (isRetireProposal(proposal)) {
|
|
1820
|
+
// A retire proposal's primary change deletes its target rather than
|
|
1821
|
+
// writing content (proposalContent() has nothing to read for a delete):
|
|
1822
|
+
// accept archives the file, it never replaces it with a blank one, so the
|
|
1823
|
+
// diff shows the file leaving — not a "proposed" side (#997).
|
|
1824
|
+
return {
|
|
1825
|
+
existing,
|
|
1826
|
+
proposed: "",
|
|
1827
|
+
unified: formatRetireDiff(proposal.ref, existing, proposal.retirement?.successorRef),
|
|
1828
|
+
isNew: false,
|
|
1829
|
+
...(targetPath ? { targetPath } : {}),
|
|
1830
|
+
};
|
|
1831
|
+
}
|
|
1832
|
+
const proposed = proposalContent(proposal);
|
|
1605
1833
|
return {
|
|
1606
1834
|
existing,
|
|
1607
1835
|
proposed,
|
|
@@ -168,11 +168,11 @@ function parseWriteRefs(rawRefs, flag) {
|
|
|
168
168
|
}
|
|
169
169
|
return parsedRefs;
|
|
170
170
|
}
|
|
171
|
-
function resolveWriteRefRoots(target) {
|
|
171
|
+
function resolveWriteRefRoots(target, targetFlag) {
|
|
172
172
|
const cfg = loadConfig();
|
|
173
173
|
let writeTarget;
|
|
174
174
|
try {
|
|
175
|
-
writeTarget = resolveWriteTarget(cfg, target);
|
|
175
|
+
writeTarget = resolveWriteTarget(cfg, target, { flag: targetFlag });
|
|
176
176
|
}
|
|
177
177
|
catch (error) {
|
|
178
178
|
if (!target)
|
|
@@ -297,11 +297,11 @@ export const XREF_SOFT_CAP = 5;
|
|
|
297
297
|
* {@link XREF_SOFT_CAP} refs emits a stderr warning (soft cap) but still
|
|
298
298
|
* returns them all.
|
|
299
299
|
*/
|
|
300
|
-
export function resolveXrefsForWrite(rawXrefs, target) {
|
|
300
|
+
export function resolveXrefsForWrite(rawXrefs, target, targetFlag) {
|
|
301
301
|
const parsedRefs = parseWriteRefs(rawXrefs, "--xref");
|
|
302
302
|
if (parsedRefs.length === 0)
|
|
303
303
|
return [];
|
|
304
|
-
const { roots } = resolveWriteRefRoots(target);
|
|
304
|
+
const { roots } = resolveWriteRefRoots(target, targetFlag);
|
|
305
305
|
const unresolved = [];
|
|
306
306
|
const xrefs = [];
|
|
307
307
|
for (const parsed of parsedRefs) {
|
|
@@ -386,13 +386,13 @@ function isParseableYamlMapping(frontmatter) {
|
|
|
386
386
|
}
|
|
387
387
|
}
|
|
388
388
|
/** Resolve any qualified supersedes ref as the mutation target for remember/import. */
|
|
389
|
-
export function resolveSupersedesWriteTarget(rawRefs, target) {
|
|
389
|
+
export function resolveSupersedesWriteTarget(rawRefs, target, targetFlag) {
|
|
390
390
|
const config = loadConfig();
|
|
391
391
|
let effectiveTarget = target;
|
|
392
392
|
for (const parsed of parseWriteRefs(rawRefs, "--supersedes")) {
|
|
393
393
|
if (!parsed.origin)
|
|
394
394
|
continue;
|
|
395
|
-
const resolved = resolveMutationTarget(config, { type: parsed.type, name: parsed.name, origin: parsed.origin }, effectiveTarget).target;
|
|
395
|
+
const resolved = resolveMutationTarget(config, { type: parsed.type, name: parsed.name, origin: parsed.origin }, effectiveTarget, { flag: targetFlag }).target;
|
|
396
396
|
effectiveTarget = resolved.selector ?? resolved.source.name;
|
|
397
397
|
}
|
|
398
398
|
return effectiveTarget;
|
|
@@ -428,17 +428,17 @@ const SUPERSEDE_REJECTED_TYPES = new Set(["secret", "env", "task", "script"]);
|
|
|
428
428
|
* constraint (and never dirtying a non-target source outside its boundary
|
|
429
429
|
* commit). A ref that resolves only in another configured source — read-only
|
|
430
430
|
* OR writable-but-not-the-target — is returned with `writable: false` and a
|
|
431
|
-
* reason (naming the
|
|
431
|
+
* reason (naming the target-flag remedy when the source is writable); the
|
|
432
432
|
* caller writes the correction anyway and reports the demotion as not
|
|
433
433
|
* applied.
|
|
434
434
|
*
|
|
435
435
|
* Returns the deduplicated plan in argv order; empty input returns [].
|
|
436
436
|
*/
|
|
437
|
-
export function resolveSupersedesForWrite(rawRefs, target) {
|
|
437
|
+
export function resolveSupersedesForWrite(rawRefs, target, targetFlag) {
|
|
438
438
|
const parsedRefs = parseWriteRefs(rawRefs, "--supersedes");
|
|
439
439
|
if (parsedRefs.length === 0)
|
|
440
440
|
return [];
|
|
441
|
-
const { roots } = resolveWriteRefRoots(target);
|
|
441
|
+
const { roots } = resolveWriteRefRoots(target, targetFlag);
|
|
442
442
|
const plan = [];
|
|
443
443
|
const unresolved = [];
|
|
444
444
|
for (const parsed of parsedRefs) {
|
|
@@ -491,7 +491,7 @@ export function resolveSupersedesForWrite(rawRefs, target) {
|
|
|
491
491
|
: {
|
|
492
492
|
reason: namedWritableSource
|
|
493
493
|
? `resolves outside the write target and the working stash, in writable source "${namedWritableSource}" at ${root}; ` +
|
|
494
|
-
`re-run with --target ${namedWritableSource} to demote it there`
|
|
494
|
+
`re-run with ${targetFlag ?? "--target"} ${namedWritableSource} to demote it there`
|
|
495
495
|
: `resolves outside the write target and the working stash, in a read-only source at ${root}; ` +
|
|
496
496
|
"demotion only applies to assets in the write target or the working stash",
|
|
497
497
|
}),
|
|
@@ -518,7 +518,9 @@ export async function writeMarkdownAsset(options) {
|
|
|
518
518
|
const subPath = normalizeCreateSubPath(options.path);
|
|
519
519
|
const baseName = normalizeMarkdownAssetName(options.name, inferAssetName(options.content, options.fallbackPrefix, options.preferredName));
|
|
520
520
|
const normalizedName = combineCreatePath(subPath, baseName);
|
|
521
|
-
const resolved = resolveMutationTarget(cfg, { type: options.type, name: normalizedName }, options.target
|
|
521
|
+
const resolved = resolveMutationTarget(cfg, { type: options.type, name: normalizedName }, options.target, {
|
|
522
|
+
flag: options.targetFlag,
|
|
523
|
+
});
|
|
522
524
|
const { target } = resolved;
|
|
523
525
|
const { source, config } = target;
|
|
524
526
|
const typeRoot = path.join(source.path, options.type === "knowledge" ? "knowledge" : "memories");
|
|
@@ -33,6 +33,8 @@ async function fetchSimilarMemories(query, excludeRef, eventSource) {
|
|
|
33
33
|
return [];
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
|
+
/** The flag `remember` takes for its destination, as its errors spell it. */
|
|
37
|
+
const TARGET_FLAG = "--bundle";
|
|
36
38
|
/**
|
|
37
39
|
* `--target` was renamed to `--bundle` on `remember` in 0.9 (S8). citty is
|
|
38
40
|
* non-strict, so the retired spelling is silently absorbed rather than
|
|
@@ -153,8 +155,8 @@ export const rememberCommand = defineJsonCommand({
|
|
|
153
155
|
// untouched. Refs resolvable only in a configured extra stash source are
|
|
154
156
|
// accepted (cross-stash provenance).
|
|
155
157
|
const rawSupersedes = parseAllFlagValues("--supersedes");
|
|
156
|
-
const writeTarget = resolveSupersedesWriteTarget(rawSupersedes, args.bundle);
|
|
157
|
-
const xrefs = resolveXrefsForWrite(parseAllFlagValues("--xref"), writeTarget);
|
|
158
|
+
const writeTarget = resolveSupersedesWriteTarget(rawSupersedes, args.bundle, TARGET_FLAG);
|
|
159
|
+
const xrefs = resolveXrefsForWrite(parseAllFlagValues("--xref"), writeTarget, TARGET_FLAG);
|
|
158
160
|
// Collect and validate --supersedes occurrences (repeatable). Same
|
|
159
161
|
// before-any-write validation contract: an unresolvable ref exits 2 with
|
|
160
162
|
// nothing written AND nothing demoted (no partial correction). The
|
|
@@ -162,7 +164,7 @@ export const rememberCommand = defineJsonCommand({
|
|
|
162
164
|
// (correction provenance per the back-linking conventions); the demotion
|
|
163
165
|
// itself runs inside writeMarkdownAsset, ordered before the git boundary
|
|
164
166
|
// commit.
|
|
165
|
-
const supersedes = resolveSupersedesForWrite(rawSupersedes, writeTarget);
|
|
167
|
+
const supersedes = resolveSupersedesForWrite(rawSupersedes, writeTarget, TARGET_FLAG);
|
|
166
168
|
for (const s of supersedes) {
|
|
167
169
|
if (!xrefs.includes(s.ref))
|
|
168
170
|
xrefs.push(s.ref);
|
|
@@ -206,6 +208,7 @@ export const rememberCommand = defineJsonCommand({
|
|
|
206
208
|
preferredName: inferAssetName(body, "memory"),
|
|
207
209
|
force: args.force,
|
|
208
210
|
target: writeTarget,
|
|
211
|
+
targetFlag: TARGET_FLAG,
|
|
209
212
|
path: args.path,
|
|
210
213
|
supersedes,
|
|
211
214
|
});
|
|
@@ -313,6 +316,7 @@ export const rememberCommand = defineJsonCommand({
|
|
|
313
316
|
preferredName: inferAssetName(body, "memory"),
|
|
314
317
|
force: args.force,
|
|
315
318
|
target: writeTarget,
|
|
319
|
+
targetFlag: TARGET_FLAG,
|
|
316
320
|
path: args.path,
|
|
317
321
|
supersedes,
|
|
318
322
|
});
|
|
@@ -39,7 +39,7 @@ export async function akmClone(options) {
|
|
|
39
39
|
// are not bundle slugs).
|
|
40
40
|
const parsed = parseQualifiedRefInput(options.sourceRef);
|
|
41
41
|
const config = hasUnmanagedDest ? undefined : loadConfig();
|
|
42
|
-
const resolvedWriteTarget = config ? resolveWriteTarget(config, options.target) : undefined;
|
|
42
|
+
const resolvedWriteTarget = config ? resolveWriteTarget(config, options.target, { flag: "--bundle" }) : undefined;
|
|
43
43
|
const writeTarget = resolvedWriteTarget ? prepareWriteTargetForMutation(resolvedWriteTarget) : undefined;
|
|
44
44
|
// An unmanaged --dest does not require any configured write target.
|
|
45
45
|
let allSources;
|
|
@@ -212,7 +212,7 @@ const tasksAddCommand = defineJsonCommand({
|
|
|
212
212
|
},
|
|
213
213
|
command: {
|
|
214
214
|
type: "string",
|
|
215
|
-
description: 'Exact shell string to run on the schedule (no AI agent), e.g. "akm improve --strategy
|
|
215
|
+
description: 'Exact shell string to run on the schedule (no AI agent), e.g. "akm improve --strategy reflect-distill".',
|
|
216
216
|
},
|
|
217
217
|
engine: { type: "string", description: "Engine to use for prompt targets (default: defaults.engine)" },
|
|
218
218
|
model: { type: "string", description: "Model override for prompt targets" },
|
|
@@ -688,7 +688,7 @@ function taskAssetRef(id) {
|
|
|
688
688
|
/** The bundle `task add` writes into: its write target, config, stash path, and name. */
|
|
689
689
|
function resolveTaskBundle(target) {
|
|
690
690
|
const config = loadConfig();
|
|
691
|
-
const resolved = prepareWriteTargetForMutation(resolveWriteTarget(config, target, { requireWritable: true }));
|
|
691
|
+
const resolved = prepareWriteTargetForMutation(resolveWriteTarget(config, target, { requireWritable: true, flag: "--bundle" }));
|
|
692
692
|
return { resolved, config, stashDir: resolved.source.path, bundleName: resolved.source.name };
|
|
693
693
|
}
|
|
694
694
|
function taskProjectionAssetResolver(config, bundleName, bundleRoot) {
|
|
@@ -696,7 +696,8 @@ function taskProjectionAssetResolver(config, bundleName, bundleRoot) {
|
|
|
696
696
|
if (bundle === bundleName) {
|
|
697
697
|
return { file: await resolveAssetPath(bundleRoot, type, name), bundleRoot };
|
|
698
698
|
}
|
|
699
|
-
|
|
699
|
+
// `bundle` is the qualifier of an asset ref in the task, not a flag.
|
|
700
|
+
const target = resolveWriteTarget(config, bundle, { requireWritable: false, flag: "The asset ref's bundle" });
|
|
700
701
|
return {
|
|
701
702
|
file: await resolveAssetPath(target.source.path, type, name),
|
|
702
703
|
bundleRoot: target.source.path,
|
|
@@ -723,7 +724,13 @@ export function resolveTaskReadBundle(refBundle, flagBundle) {
|
|
|
723
724
|
else {
|
|
724
725
|
const configured = resolveActiveConfiguredSources(config).some((source) => source.name === selector);
|
|
725
726
|
const implicit = configured ? undefined : resolveImplicitScheduledBundleTarget(config, selector);
|
|
726
|
-
resolved =
|
|
727
|
+
resolved =
|
|
728
|
+
implicit ??
|
|
729
|
+
resolveWriteTarget(config, selector, {
|
|
730
|
+
requireWritable: false,
|
|
731
|
+
// The selector is `--bundle` when given, else the task ref's own bundle qualifier.
|
|
732
|
+
flag: flagBundle !== undefined ? "--bundle" : "The task ref's bundle",
|
|
733
|
+
});
|
|
727
734
|
}
|
|
728
735
|
if (refBundle && resolved.source.name !== refBundle) {
|
|
729
736
|
throw new UsageError(`Task ref bundle ${JSON.stringify(refBundle)} does not match the resolved source.`, "INVALID_FLAG_VALUE");
|
|
@@ -50,9 +50,14 @@ function resolveExplicitMutationTarget(config, explicitTarget, options) {
|
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
|
-
/**
|
|
53
|
+
/**
|
|
54
|
+
* Reconcile a qualified mutation ref with the explicit target (`--target`, or
|
|
55
|
+
* the `options.flag` the caller's command spells), then resolve the write
|
|
56
|
+
* destination.
|
|
57
|
+
*/
|
|
54
58
|
export function resolveMutationTarget(config, ref, explicitTarget, options = {}) {
|
|
55
|
-
const
|
|
59
|
+
const flag = options.flag ?? "--target";
|
|
60
|
+
const writeOptions = { requireWritable: options.requireWritable, flag };
|
|
56
61
|
const qualifiedTarget = ref.origin ? resolveBundleWriteTarget(config, ref.origin, writeOptions) : undefined;
|
|
57
62
|
const explicitResolved = explicitTarget
|
|
58
63
|
? resolveExplicitMutationTarget(config, explicitTarget, writeOptions)
|
|
@@ -60,7 +65,7 @@ export function resolveMutationTarget(config, ref, explicitTarget, options = {})
|
|
|
60
65
|
if (qualifiedTarget &&
|
|
61
66
|
explicitResolved &&
|
|
62
67
|
path.resolve(qualifiedTarget.source.path) !== path.resolve(explicitResolved.source.path)) {
|
|
63
|
-
throw new UsageError(`Qualified ref bundle "${ref.origin}" conflicts with
|
|
68
|
+
throw new UsageError(`Qualified ref bundle "${ref.origin}" conflicts with ${flag} "${explicitTarget}".`, "INVALID_FLAG_VALUE", `Drop ${flag} or select the same bundle.`);
|
|
64
69
|
}
|
|
65
70
|
let target = qualifiedTarget ?? explicitResolved ?? resolveWriteTarget(config, undefined, writeOptions);
|
|
66
71
|
const bundleId = ref.origin ?? canonicalBundleIdForTarget(config, target);
|
|
@@ -59,16 +59,17 @@ export function resolveWriteTarget(akmConfig, explicitTarget, options = {}) {
|
|
|
59
59
|
const allConfiguredSources = resolveConfiguredSources(akmConfig);
|
|
60
60
|
const configuredSources = resolveActiveConfiguredSources(akmConfig);
|
|
61
61
|
const requireWritable = options.requireWritable !== false;
|
|
62
|
+
const flag = options.flag ?? "--target";
|
|
62
63
|
if (explicitTarget) {
|
|
63
64
|
const match = configuredSources.find((s) => s.name === explicitTarget);
|
|
64
65
|
if (!match) {
|
|
65
66
|
if (allConfiguredSources.some((source) => source.name === explicitTarget)) {
|
|
66
67
|
throw new UsageError(`Bundle "${explicitTarget}" is disabled.`, "INVALID_FLAG_VALUE");
|
|
67
68
|
}
|
|
68
|
-
throw new UsageError(
|
|
69
|
+
throw new UsageError(`${flag} must reference a source name from your config. No source named "${explicitTarget}" is configured. Run \`akm bundle list\` to see available sources.`, "INVALID_FLAG_VALUE");
|
|
69
70
|
}
|
|
70
71
|
if (requireWritable && !resolveWritable({ type: match.type, writable: match.writable })) {
|
|
71
|
-
throw new ConfigError(`source ${explicitTarget} is not writable`, "INVALID_CONFIG_FILE", `Set \`writable: true\` on the "${explicitTarget}" source in your config, or pass
|
|
72
|
+
throw new ConfigError(`source ${explicitTarget} is not writable`, "INVALID_CONFIG_FILE", `Set \`writable: true\` on the "${explicitTarget}" source in your config, or pass ${flag} to a different source.`);
|
|
72
73
|
}
|
|
73
74
|
return adaptConfiguredSource(match);
|
|
74
75
|
}
|
|
@@ -85,7 +85,8 @@ export function countFeedbackSignals(db, entryId) {
|
|
|
85
85
|
* Count usage events of a given `event_type`.
|
|
86
86
|
*
|
|
87
87
|
* Lifted verbatim from `akm improve` (improve.ts) where the show-event count
|
|
88
|
-
* was hand-rolled inline to drive the
|
|
88
|
+
* was hand-rolled inline to drive the warning that the retrieval scope matches
|
|
89
|
+
* only search-retrieved assets.
|
|
89
90
|
*/
|
|
90
91
|
export function countUsageEventsByType(db, eventType) {
|
|
91
92
|
return db.prepare("SELECT COUNT(*) AS cnt FROM usage_events WHERE event_type = ?").get(eventType)
|
|
@@ -86,6 +86,7 @@ export function shapeProposalEntry(entry, detail) {
|
|
|
86
86
|
"confidence",
|
|
87
87
|
"gateDecision",
|
|
88
88
|
"review",
|
|
89
|
+
"reviewHistory",
|
|
89
90
|
"retirement",
|
|
90
91
|
]);
|
|
91
92
|
}
|
|
@@ -102,6 +103,7 @@ export function shapeProposalEntry(entry, detail) {
|
|
|
102
103
|
"gateDecision",
|
|
103
104
|
"payload",
|
|
104
105
|
"review",
|
|
106
|
+
"reviewHistory",
|
|
105
107
|
"retirement",
|
|
106
108
|
"retiredArchive",
|
|
107
109
|
"promotionSource",
|
|
@@ -167,6 +169,11 @@ export function shapeProposalDiffOutput(result, detail) {
|
|
|
167
169
|
isNew: result.isNew,
|
|
168
170
|
unified: result.unified,
|
|
169
171
|
...(result.targetPath !== undefined ? { targetPath: result.targetPath } : {}),
|
|
172
|
+
// A retire proposal (#997): the diff alone is a file leaving, so the pair
|
|
173
|
+
// verdict and the accept/revert note travel with it at every detail level.
|
|
174
|
+
...(result.op !== undefined ? { op: result.op } : {}),
|
|
175
|
+
...(result.retirement !== undefined ? { retirement: result.retirement } : {}),
|
|
176
|
+
...(result.note !== undefined ? { note: result.note } : {}),
|
|
170
177
|
};
|
|
171
178
|
if (detail === "full") {
|
|
172
179
|
return { schemaVersion: result.schemaVersion, ...base };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
// Output shape registration for `akm proposal reopen` (#997). One reopened
|
|
5
|
+
// proposal is the same envelope `reject` returns (`ok`, `id`, `ref`, an
|
|
6
|
+
// optional `reason` — here the reopen reason — and the shaped proposal), so it
|
|
7
|
+
// shares that shaper; several are the passthrough `proposal-reopen-batch`.
|
|
8
|
+
import { shapeProposalRejectOutput } from "../helpers.js";
|
|
9
|
+
export const proposalReopenShapes = [
|
|
10
|
+
{
|
|
11
|
+
command: "proposal-reopen",
|
|
12
|
+
handler: (result, detail) => shapeProposalRejectOutput(result, detail),
|
|
13
|
+
},
|
|
14
|
+
];
|