Skip to content
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
124c6bd
plan-alm: convert to strict plan-only planner (execution skills self-…
tyaginidhi Jun 17, 2026
4d50c9e
Address Copilot review on #191
tyaginidhi Jun 17, 2026
2fd9aa6
Self-review fixes: PP-path activate step, manual steps[], EDM error rule
tyaginidhi Jun 17, 2026
298fcf0
Address Copilot second-pass review on #191
tyaginidhi Jun 17, 2026
9f66e35
Fix step-sync stage matching: marker "Deploy to {label}" vs plan step…
tyaginidhi Jun 17, 2026
16f3d32
Address Copilot third-pass: stable reconcile() nextStep contract
tyaginidhi Jun 17, 2026
be368a6
Harden stage matching: shared normalizeStageLabel for keys too
tyaginidhi Jun 17, 2026
776356d
Potential fix for pull request finding
tyaginidhi Jun 17, 2026
e59db35
Address Copilot fourth-pass on #191
tyaginidhi Jun 17, 2026
ed2ad59
EDM (enhanced/standard data-model) Power Pages site support in ALM di…
tyaginidhi Jun 17, 2026
c5db654
Site-referenced table discovery + dependency-aware solution splitting
tyaginidhi Jun 17, 2026
56b996f
Fix FFD bin-packing under-allocation that overflowed the schema-attr cap
tyaginidhi Jun 17, 2026
5e3d1ab
Wire --projectRoot into the remaining discover-site-components consumers
tyaginidhi Jun 17, 2026
696d41c
Enforce ALM plan refresh via PostToolUse reconcile backstop (auto-heal)
tyaginidhi Jun 17, 2026
e74e4b7
Surface reconcile failures in the hook + document failed[] contract
tyaginidhi Jun 17, 2026
a5e20e7
Fix stale "plan-alm Phases 6/7/8" reference in AGENTS.md
tyaginidhi Jun 17, 2026
dfb672a
Activate the PLAN_STATUS lifecycle: Approved -> In Execution -> Compl…
tyaginidhi Jun 17, 2026
3db879d
Surface COMPLETED_AT in the ALM plan footer
tyaginidhi Jun 17, 2026
f5ca2bb
Site-referenced table discovery + dependency-aware solution splitting
tyaginidhi Jun 17, 2026
be4a70d
Fix FFD bin-packing under-allocation that overflowed the schema-attr cap
tyaginidhi Jun 17, 2026
e55e06d
Wire --projectRoot into the remaining discover-site-components consumers
tyaginidhi Jun 17, 2026
bb6f7f7
Address #193 review: attr-cap overflow warning + componentCount proxy…
tyaginidhi Jun 18, 2026
6e8b081
Resolve merge conflicts with origin/users/nityagi/table-discovery-fix
Copilot Jun 18, 2026
91be9fd
Resolve merge conflicts with origin/users/nityagi/table-discovery-fix…
Copilot Jun 18, 2026
525405a
Merge main into branch: resolve conflicts after stacked PRs landed
Copilot Jun 19, 2026
673ae86
Fix incomplete Open Plugins resolution in the merge (#194)
tyaginidhi Jun 19, 2026
1f5ddd2
Address #194 review: DRY plan path + hook smoke test + completion-edg…
tyaginidhi Jun 22, 2026
f27128f
Address #194 Copilot review comments (3)
tyaginidhi Jun 22, 2026
af087e7
plan-alm: move PLAN_STATUS badge next to the "Generated" line
tyaginidhi Jun 22, 2026
dfb5023
Address #194 review: don't let Approved->In Execution promotion mask …
tyaginidhi Jun 22, 2026
c5bb5a0
Merge remote-tracking branch 'origin/main' into users/nityagi/plan-re…
tyaginidhi Jun 22, 2026
b018fa7
Sync legacy JSON mirrors to power-pages v2.5.0 after merging #203
tyaginidhi Jun 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"source": "./plugins/power-pages",
"description": "Power Pages development and management plugin for Claude Code and GitHub Copilot",
"category": "development",
"version": "2.4.0",
"version": "2.5.0",
"license": "MIT",
"tags": [
"power platform",
Expand Down
2 changes: 1 addition & 1 deletion plugins/power-pages/.plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "power-pages",
"version": "2.4.0",
"version": "2.5.0",
"description": "Create and deploy Power Pages sites using modern development approaches. Supports code sites (SPAs) with React, Angular, Vue, or Astro. Includes ALM orchestration (plan-alm) with a solution-splitting decision tree, per-solution pipelines, Azure Blob asset advisory, manifest schema v2 for multi-solution deployments, and force-link remediation for cross-host pipeline migrations.",
"author": {
"name": "Microsoft",
Expand Down
6 changes: 4 additions & 2 deletions plugins/power-pages/AGENTS.md

Large diffs are not rendered by default.

48 changes: 47 additions & 1 deletion plugins/power-pages/hooks/run-skill-posttool-validation.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
#!/usr/bin/env node

const fs = require('fs');
const path = require('path');
const { spawnSync } = require('child_process');
const {
getTrackedSkillFromToolInput,
getValidatorScript,
isAlmPlanSkill,
} = require('../scripts/lib/powerpages-hook-utils');

const DEBUG = process.env.DEBUG === '1' || process.env.DEBUG === 'true';
Expand Down Expand Up @@ -36,19 +38,63 @@ process.stdin.on('end', () => {
process.exit(0);
}

const cwd = input.cwd || process.cwd();

const validatorScript = getValidatorScript(skillName);
if (validatorScript) {
const validatorPath = path.join(__dirname, '..', validatorScript);
const result = spawnSync(process.execPath, [validatorPath], {
input: inputData,
encoding: 'utf8',
cwd: input.cwd || process.cwd(),
cwd,
});
if (result.stdout) process.stdout.write(result.stdout);
if (result.stderr) process.stderr.write(result.stderr);
validatorStatus = result.status ?? 0;
debug(`[power-pages hook] Validator exited with code ${validatorStatus}\n`);
}

// ALM plan reconcile backstop (auto-heal). The refresh-alm-plan-data.js calls
// in each SKILL.md are advisory — silently dropped on session fragmentation,
// manual execution, or oversight. After ANY ALM plan skill completes, reconcile
// the plan against the marker files: any marker newer than the plan (a skipped
// refresh) is ingested automatically. Best-effort and NON-blocking — it never
// changes the hook's exit code (the validator's status stands). Triggering on
// any ALM skill (not just the marker's writer) catches a skip that surfaces only
// when the NEXT ALM skill runs. Honors .alm-deferred + no-plan inside reconcile.
if (isAlmPlanSkill(skillName) && fs.existsSync(path.join(cwd, 'docs', '.alm-plan-data.json'))) {
try {
const refreshPath = path.join(__dirname, '..', 'scripts', 'lib', 'refresh-alm-plan-data.js');
const rec = spawnSync(process.execPath, [refreshPath, '--projectRoot', cwd, '--reconcile', '--render'], {
encoding: 'utf8',
cwd,
timeout: 20000,
});
let reconciled = [];
let failed = [];
try {
const out = JSON.parse((rec.stdout || '').trim());
reconciled = out.reconciled || [];
failed = out.failed || [];
} catch {}
Comment thread
tyaginidhi marked this conversation as resolved.
Outdated
if (reconciled.length > 0) {
process.stdout.write(
`[power-pages] ALM plan was out of sync with ${reconciled.length} run marker(s) — refreshed automatically (${reconciled.join(', ')}).\n`,
);
}
if (failed.length > 0) {
// Non-blocking, but surfaced — a swallowed reconcile failure is exactly
// what makes a stale plan impossible to diagnose.
process.stdout.write(
`[power-pages] ALM plan reconcile could not heal ${failed.length} phase(s): ${failed.map((f) => f.phase).join(', ')}. See stderr for details.\n`,
);
}
debug(`[power-pages hook] reconcile reconciled=${JSON.stringify(reconciled)} failed=${JSON.stringify(failed)}\n`);
} catch (e) {
// Best-effort — a reconcile failure must never break the skill or the hook.
debug(`[power-pages hook] reconcile error (ignored): ${e.message}\n`);
}
}
} catch (err) {
process.stderr.write(`[power-pages hook] Unexpected error: ${err.message}\n`);
validatorStatus = 0;
Expand Down
17 changes: 16 additions & 1 deletion plugins/power-pages/scripts/lib/check-alm-plan.js
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,22 @@ async function checkAlmPlan({ projectRoot, envUrl, token, solutionId, makeReques
// way each in-chain skill's Phase 0 call both observes the prior heartbeat
// (for its own decision) AND keeps the chain alive for the next skill.
const priorLastInvocationAt = planData.LAST_INVOCATION_AT || null;
const planStatus = planData.PLAN_STATUS || null;
let planStatus = planData.PLAN_STATUS || null;

// Promote Approved -> In Execution on the FIRST execution-skill Phase 0 entry.
// plan-alm is plan-only: it leaves the plan "Approved" and the user runs the
// execution skills themselves. The first execution skill to reach its Phase 0
// gate is what actually starts execution, so flip the plan to "In Execution"
// here — this is the transition that activates the heartbeat/active-chain
// machinery (nothing else sets it). The heartbeat write below then persists
// both the new status AND the first heartbeat in one atomic write. Gated on
// `writeHeartbeat` so read-only callers (--no-heartbeat: audits, tests, and
// plan-alm's own deferral check) never mutate the plan's status.
if (writeHeartbeat && planStatus === 'Approved') {
planStatus = 'In Execution';
planData.PLAN_STATUS = 'In Execution';
}

const inExecution = computeInExecution(planStatus, priorLastInvocationAt, nowMs);

if (writeHeartbeat && planStatus === 'In Execution') {
Expand Down
31 changes: 31 additions & 0 deletions plugins/power-pages/scripts/lib/powerpages-hook-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,40 @@ function getValidatorScript(skillName) {
return TRACKED_SKILLS[skillName]?.validatorScript ?? null;
}

// Skills that write a `docs/alm/last-*.json` marker or otherwise consume the ALM
// plan. After any of these completes, the PostToolUse hook runs a plan reconcile
// (auto-heal) — so a refresh step skipped by ONE skill is caught when the NEXT
// ALM skill completes (covers manual/cross-session execution).
const ALM_PLAN_SKILLS = new Set([
'setup-solution',
'setup-pipeline',
'deploy-pipeline',
'export-solution',
'import-solution',
'configure-env-variables',
'activate-site',
'test-site',
'ensure-pipelines-host',
'force-link-environment',
]);

/**
* True when `value` (a raw skill name, `/skill`, or `power-pages:skill`) resolves
* to an ALM plan skill. Normalizes via `detectTrackedSkill`, so it also confirms
* the skill actually exists in this plugin.
* @param {string} value
* @returns {boolean}
*/
Comment thread
tyaginidhi marked this conversation as resolved.
function isAlmPlanSkill(value) {
const name = detectTrackedSkill(value);
return name != null && ALM_PLAN_SKILLS.has(name);
}

module.exports = {
TRACKED_SKILLS,
ALM_PLAN_SKILLS,
detectTrackedSkill,
getTrackedSkillFromToolInput,
getValidatorScript,
isAlmPlanSkill,
};
36 changes: 36 additions & 0 deletions plugins/power-pages/scripts/lib/refresh-alm-plan-data.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@
// finalize:
// - PLAN_STATUS = "Completed"
//
// After every phase's step-sync, a completion evaluator flips PLAN_STATUS to
// "Completed" (+ COMPLETED_AT) once all non-skip steps are completed and none
// failed — so the last execution skill terminates the plan automatically,
// without any skill needing to call the explicit "finalize" phase. The
// Approved -> In Execution promotion that starts the lifecycle lives in
// check-alm-plan.js (first execution-skill Phase 0).
//
// stdout JSON includes `nextStep: { name, skill: string | null } | null` (when ok:true) — the first
// still-pending checklist step and the slash command that runs it. Execution
// skills echo this so the user knows the next step to invoke (user-driven
Expand Down Expand Up @@ -527,6 +534,33 @@ function refreshFinalize(planData) {
return planData;
}

// Completion evaluator. Once every non-skipped checklist step is `completed`
// (and none `failed`), the plan has been fully executed — transition it to
// "Completed" + stamp COMPLETED_AT. Runs after each phase's step-sync (in both
// refresh() and reconcile()), so the LAST execution skill to finish flips the
// plan terminal automatically — no skill has to call `--phase finalize`
// explicitly (nothing did, so the lifecycle previously never completed). Only
// advances from a pre-terminal, non-draft state: "In Execution" (the normal
// case after check-alm-plan.js promoted it on the first execution skill) or
// "Approved" (defensive fallback if that promotion didn't run). Never regresses
// a Draft or an already-Completed plan, and a `failed` step blocks completion
// (e.g. a failed deploy must not look "done" just because later steps are
// pending-skipped).
function evaluatePlanCompletion(planData) {
if (!planData) return;
const status = planData.PLAN_STATUS;
if (status !== 'In Execution' && status !== 'Approved') return;
if (!Array.isArray(planData.steps)) return;
const live = planData.steps.filter((s) => s && s.skip !== true && typeof s.name === 'string');
if (live.length === 0) return;
const anyFailed = live.some((s) => s.status === 'failed');
const allCompleted = live.every((s) => s.status === 'completed');
if (!anyFailed && allCompleted) {
planData.PLAN_STATUS = 'Completed';
planData.COMPLETED_AT = new Date().toISOString();
}
}

// Refresh-phase helper: stamp `LAST_SYNC_AT` on planData so check-alm-plan.js's
// freshness check correctly accounts for source-solution modifications caused
// by the just-completed phase. Called by every phase that touches the source
Expand Down Expand Up @@ -1085,6 +1119,7 @@ function reconcile({ projectRoot, render, rendererPath }) {
process.stderr.write(`[refresh-alm-plan-data] reconcile phase "${phase}" failed: ${e.message}\n`);
}
}
evaluatePlanCompletion(planData);
fs.writeFileSync(dataPath, JSON.stringify(planData, null, 2), 'utf8');

let rendered = false;
Expand Down Expand Up @@ -1135,6 +1170,7 @@ function refresh({ projectRoot, phase, render, rendererPath, stageName }) {
}

applyRefresh(planData, phase, projectRoot, stageName);
evaluatePlanCompletion(planData);
fs.writeFileSync(dataPath, JSON.stringify(planData, null, 2), 'utf8');

let rendered = false;
Expand Down
44 changes: 43 additions & 1 deletion plugins/power-pages/scripts/tests/check-alm-plan.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ test('returns exists:true / stale:false when plan exists and no env credentials'
PLAN_STATUS: 'Approved',
});
try {
const r = await checkAlmPlan({ projectRoot: dir });
// writeHeartbeat:false → read-only check; an Approved plan is NOT promoted.
const r = await checkAlmPlan({ projectRoot: dir, writeHeartbeat: false });
assert.equal(r.exists, true);
assert.equal(r.stale, false);
assert.equal(r.generatedAt, '2026-04-01T00:00:00.000Z');
Expand Down Expand Up @@ -515,3 +516,44 @@ test('Unparseable LAST_SYNC_AT falls back to GENERATED_AT (defensive)', async ()
fs.rmSync(dir, { recursive: true, force: true });
}
});

// --- Approved -> In Execution promotion (lifecycle activation) ---------------

test('promotes Approved -> In Execution on the first execution-skill Phase 0 (writeHeartbeat default)', async () => {
const dir = tempProject({ SITE_NAME: 'T', PLAN_STATUS: 'Approved' });
try {
const r = await checkAlmPlan({ projectRoot: dir });
assert.equal(r.planStatus, 'In Execution', 'returned status reflects the promotion');
assert.equal(r.inExecution.status, 'active', 'newly In Execution with first heartbeat is active');
// Promotion + heartbeat are persisted to disk for the next in-chain skill.
const onDisk = JSON.parse(fs.readFileSync(path.join(dir, 'docs', '.alm-plan-data.json'), 'utf8'));
assert.equal(onDisk.PLAN_STATUS, 'In Execution');
assert.ok(onDisk.LAST_INVOCATION_AT, 'first heartbeat written');
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
});

test('does NOT promote Approved when writeHeartbeat is false (read-only callers: plan-alm, audits, tests)', async () => {
const dir = tempProject({ SITE_NAME: 'T', PLAN_STATUS: 'Approved' });
try {
const r = await checkAlmPlan({ projectRoot: dir, writeHeartbeat: false });
assert.equal(r.planStatus, 'Approved', 'read-only check leaves the plan Approved');
const onDisk = JSON.parse(fs.readFileSync(path.join(dir, 'docs', '.alm-plan-data.json'), 'utf8'));
assert.equal(onDisk.PLAN_STATUS, 'Approved', 'disk untouched');
assert.equal(onDisk.LAST_INVOCATION_AT, undefined, 'no heartbeat written');
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
});

test('does NOT promote a Draft plan (only Approved is promotable)', async () => {
const dir = tempProject({ SITE_NAME: 'T', PLAN_STATUS: 'Draft' });
try {
const r = await checkAlmPlan({ projectRoot: dir });
assert.equal(r.planStatus, 'Draft');
assert.equal(r.inExecution.status, 'not-running');
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
});
26 changes: 26 additions & 0 deletions plugins/power-pages/scripts/tests/powerpages-hook-utils.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ const path = require('path');

const {
TRACKED_SKILLS,
ALM_PLAN_SKILLS,
detectTrackedSkill,
getTrackedSkillFromToolInput,
getValidatorScript,
isAlmPlanSkill,
} = require('../lib/powerpages-hook-utils');

const SKILLS_DIR = path.join(__dirname, '..', '..', 'skills');
Expand Down Expand Up @@ -148,3 +150,27 @@ test('Object.prototype keys are not mistaken for tracked skills', () => {
assert.equal(detectTrackedSkill('__proto__'), null);
assert.equal(getTrackedSkillFromToolInput({ skill: 'toString' }), null);
});

// --- ALM_PLAN_SKILLS / isAlmPlanSkill (reconcile-trigger gating) -------------

test('isAlmPlanSkill: true for ALM plan skills, normalizing prefixes', () => {
assert.equal(isAlmPlanSkill('activate-site'), true);
assert.equal(isAlmPlanSkill('/power-pages:activate-site'), true);
assert.equal(isAlmPlanSkill('/activate-site'), true);
assert.equal(isAlmPlanSkill('ensure-pipelines-host'), true);
assert.equal(isAlmPlanSkill('setup-pipeline'), true);
});

test('isAlmPlanSkill: false for non-ALM skills and junk', () => {
assert.equal(isAlmPlanSkill('create-site'), false);
assert.equal(isAlmPlanSkill('add-seo'), false);
assert.equal(isAlmPlanSkill('not-a-real-skill'), false);
assert.equal(isAlmPlanSkill(null), false);
assert.equal(isAlmPlanSkill(undefined), false);
});

test('ALM_PLAN_SKILLS members are all real tracked skills', () => {
for (const name of ALM_PLAN_SKILLS) {
assert.ok(TRACKED_SKILLS[name], `ALM_PLAN_SKILLS member "${name}" must be a tracked skill (have a SKILL.md)`);
}
});
36 changes: 36 additions & 0 deletions plugins/power-pages/scripts/tests/render-alm-plan.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1944,3 +1944,39 @@ test('render-alm-plan: checklist link onclick reuses the existing data-tab click
fs.rmSync(tmpDir, { recursive: true, force: true });
}
});

// ── COMPLETED_AT footer line (lifecycle terminal) ────────────────────────────

test('render-alm-plan: surfaces COMPLETED_AT in the footer when the plan is Completed', () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'render-alm-completed-'));
const outputPath = path.join(tmpDir, 'alm-plan.html');
try {
const { status } = runScript(
makeValidData({ PLAN_STATUS: 'Completed', COMPLETED_AT: '2026-06-16T18:30:00.000Z' }),
outputPath,
);
assert.equal(status, 0);
const html = fs.readFileSync(outputPath, 'utf8');
assert.match(html, /id="completed-at"/, 'completed-at span rendered');
assert.match(html, /Completed:<\/strong>\s*<span id="completed-at">2026-06-16T18:30:00\.000Z<\/span>/);
assert.ok(!html.includes('__COMPLETED_LINE__'), 'no orphan placeholder');
assert.match(html, /class="plan-status completed"/, 'status badge styled completed');
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
});

test('render-alm-plan: omits the Completed footer line when COMPLETED_AT is absent', () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'render-alm-nocompleted-'));
const outputPath = path.join(tmpDir, 'alm-plan.html');
try {
const { status } = runScript(makeValidData({ PLAN_STATUS: 'In Execution' }), outputPath);
assert.equal(status, 0);
const html = fs.readFileSync(outputPath, 'utf8');
assert.ok(!html.includes('id="completed-at"'), 'no completed-at span when not completed');
assert.ok(!html.includes('__COMPLETED_LINE__'), 'placeholder still replaced (empty)');
assert.match(html, /class="plan-status in-execution"/, 'status badge styled in-execution');
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
});
11 changes: 11 additions & 0 deletions plugins/power-pages/skills/ensure-pipelines-host/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -900,6 +900,17 @@ Record skill usage:

Follow the skill tracking instructions in the reference to record this skill's usage. Use `--skillName "EnsurePipelinesHost"`.

**Refresh the ALM plan (if one exists):**

```bash
node "${PLUGIN_ROOT}/scripts/lib/refresh-alm-plan-data.js" \
--projectRoot "." \
--phase ensure-pipelines-host \
--render
```

This updates `planData.hostResolution` from the `docs/alm/last-host-check.json` you just wrote (host-only — no pipeline yet) and drops the pre-run NoHost risks, then re-renders `docs/alm-plan.html`. **Do this here, not just in setup-pipeline Phase 7** — a host install can take 18+ minutes and cross a session boundary, so deferring the refresh risks the plan never reflecting the host. When `docs/.alm-plan-data.json` is absent (standalone, not part of an ALM plan), the helper returns `ok:false` as a soft no-op. The centralized PostToolUse hook also reconciles the plan as a backstop, but refreshing at the source keeps the rendered plan current immediately.

Present summary table:

| Field | Value |
Expand Down
4 changes: 3 additions & 1 deletion plugins/power-pages/skills/plan-alm/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,11 @@ Steps:
0. **Detect prior ALM deferral for this project.** Before any discovery work, check whether the project root contains a `.alm-deferred` marker file. The marker is written by users who explicitly opted ALM-skill validators out of "missing artifacts" warnings (e.g. *"this site is handled separately"* or *"ni-dev — no ALM"*). If a user is now invoking `plan-alm`, we should surface that the marker is present and ask what to do, rather than silently proceeding (which would build a plan the user previously decided not to maintain) or silently removing the marker (which would re-enable nags on every other ALM skill).

```bash
node "${PLUGIN_ROOT}/scripts/lib/check-alm-plan.js" --projectRoot "."
node "${PLUGIN_ROOT}/scripts/lib/check-alm-plan.js" --projectRoot "." --no-heartbeat
```

> Use `--no-heartbeat` here: this is a **read-only** deferral check by the *planner*, not an execution-skill Phase 0 gate. Without it, `check-alm-plan.js` would promote an already-`Approved` plan to `In Execution` (and refresh the heartbeat) just because you re-opened `plan-alm` — but re-planning isn't execution. Execution skills call it *without* `--no-heartbeat` so the first one to run does the `Approved → In Execution` promotion.

<!-- gate: plan-alm:1.deferral | category=progress | cancel-leaves=deferral-marker -->
> 🚦 **Gate (progress · plan-alm:1.deferral):** `.alm-deferred` marker present — continue and remove, continue and keep marker, or cancel. Determines whether downstream ALM skills resume gate enforcement.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,7 @@ <h2>Execution Checklist</h2>
<div class="card" style="margin-top:20px;font-size:12px;color:var(--text-dim);">
<strong>Approved by:</strong> <span id="approved-by">__APPROVED_BY__</span> &nbsp;&middot;&nbsp;
<strong>Approval date:</strong> <span id="approval-date">__APPROVAL_DATE__</span>
__COMPLETED_LINE__
</div>
</div>

Expand Down
Loading
Loading